An Introduction to Type Safety in JavaScript With Prisma
In this article, we walk you through an example app built with Prisma, Fastify, and MySQL that implements automated checks for increased type safety.
Join the DZone community and get the full member experience.
Join For FreeIf you’ve worked with JavaScript long enough, you’ve likely run into type-related issues. For instance, you might have accidentally converted a value from an integer to a string:
> console.log("User's cart value:", "500" + 100)
[Log] User's cart value: "500100"
A seemingly innocent error can become a problem if it resides in, say, your application’s billing code. And with JavaScript increasingly being used in critical services, such a scenario is likely to happen in real life. Luckily, database tools like Prisma deliver type safety in the database access layer for your JavaScript projects.
In this article, we provide a background on typing in JavaScript and highlight the implications for real-world projects. We then walk you through an example app—built with Prisma, Fastify, and MySQL—that implements automated checks for increased type safety.
Let’s dive in!
Static vs Dynamic Typing
In different programming languages, the checking of types for variables and values can happen at different stages of the program’s compilation or execution. Languages can also allow or disallow certain operations, as well as allowing or disallowing type combinations.
Depending on when exactly the type checking happens, a programming language can be statically typed or dynamically typed. Statically-typed languages, like C++, Haskell, and Java, all generally check for type errors at compile time.
Dynamically-typed languages, like JavaScript, instead check for type errors during the program’s execution. A type error is not that easy to get in JavaScript as this language’s variables do not have types. But if we try to “accidentally” use a variable as a function, we’ll get a TypeError during the program’s run time:
// types.js
numBakers = 2;
console.log(numBakers());
The error looks as follows in our console:
TypeError: numBakers is not a function
Strong vs Weak Typing
The other axis of type checking is strong typing vs. weak typing. Here, the line between strong and weak is blurry and is defined depending on a developer’s or a community’s opinion.
Some folks say that a language is weakly-typed if it allows implicit type conversions. In JavaScript, the following code is valid even though we’re calculating a sum of an integer and a string:
numBakers = 1;
numWaiters = "many";
totalStaff = numBakers + numWaiters; // 1many
This code gets executed without errors, and the value 1 gets implicitly converted into a string when it comes to calculating the value of totalStaff.
Based on such behavior, JavaScript can be considered a weakly-typed language. But what does weak typing mean in practical terms?
For many developers, type weakness can cause discomfort and uncertainty when writing JavaScript code, especially when working with sensitive systems, like billing code or accounting data. Unexpected types in variables, if not controlled for, can cause confusion and even produce real damage.
Consider a baking equipment website that allows you to purchase a commercial-grade mixer and some replacement parts for it:
// types.js
mixerPrice = "1870";
mixerPartsPrice = 100;
console.log("Total cart value:", mixerPrice + mixerPartsPrice);
Notice that there’s a type mismatch between how the prices are defined. Possibly, an incorrect type previously got sent to the backend, and the information was incorrectly stored in the database as a result. What happens if we execute this code? Let’s run the example to illustrate the result:
$ node types.js
Total cart value: 1870100
Oof! A bakery’s owner might be a little surprised if such a charge were to hit their bank account.
How JavaScript's Lack of Type Safety Can Slow You Down
To avoid situations like the one with the shopping cart above, developers seek type safety— it guarantees that the data they operate on is of a certain type and will result in predictable behavior.
As a weakly-typed language, JavaScript does not provide type safety. Still, many production systems that handle bank balances, insurance amounts, and other sensitive data are developed in JavaScript.
Developers are wary of unexpected behavior not only because it can lead to incorrect transaction amounts. The lack of type safety in JavaScript can be an inconvenience for a variety of other reasons such as:
- Reduction of productivity: If you have to deal with type errors, debugging them and thinking through all the possibilities of type interactions going wrong can take a long time and add mental overhead.
- Boilerplate code for handling type mismatches: In type-sensitive operations, developers frequently need to add code to check for types and reconcile any possible differences. In addition, engineers have to write many tests for the handling of the undefined data type. Adding extra code that isn’t directly related to your application’s business value is not ideal for keeping your codebase readable and clean.
- Lack of clear error messages: Sometimes type mismatches can generate cryptic errors way down the line from the location of the type error. In such situations, type errors can be frustrating and difficult to debug.
- Unexpected issues when writing to databases: Type errors can cause issues when it comes to writing to your database. For example, as an application’s database evolves, developers frequently need to add new database fields. Adding a field in a staging environment but forgetting to roll it out to the production environment can lead to unexpected type errors as the production deployment goes live.
Since type errors in the database layer of your application can cause lots of harm due to data corruption, developers have to come up with solutions for the problems that lack of type safety introduces. In the next section, we discuss how introducing a tool like Prisma can help you address type safety in your JavaScript projects.
Type-Safe Database Access Using Prisma
While JavaScript itself does not offer built-in type safety, Prisma allows you to opt into type safety checks in your application. Prisma is a new kind of ORM tool that consists of a type-safe query builder for JavaScript and TypeScript (Prisma Client), a migration system (Prisma Migrate), and a GUI for interacting with your database (Prisma Studio).
At the core of the type checks in Prisma is the Prisma schema, the single source of truth where you model your data. Here’s what a minimal schema looks like:
// prisma/schema.prisma
model Baker {
id Int @id @default(autoincrement())
email String @unique
name String?
}
In this example, the schema describes a Baker entity where each instance, an individual baker, has an email (a string), a name (also a string, optional), and an automatically incremented identifier (an integer). The word “model” is used to describe a data entity that is mapped to a database table on the backend.
Under the hood, the Prisma CLI generates Prisma Client from your Prisma schema. The generated code allows you to access your database conveniently in JavaScript, and implements a series of checks and utilities to make your code type-safe. Each model that is defined in the Prisma schema gets converted into a JavaScript class with functions for accessing individual records.
By using a tool like Prisma in your project, you start taking advantage of the additional type checks when using the data types generated by the library (its object-relational mapping layer, or ORM) to access records in your database.
Example of Implementing Type Safety in a Fastify App
Let’s look at an example of using a Prisma schema in a Fastify application. Fastify is a web framework for Node.js that focuses on performance and simplicity.
We’ll use the prisma-fastify-bakery project, which implements a simple system for tracking a bakery’s operations.
Preliminary Setup
To run the project, we’ll need to have a recent Node.js version set up on our development machine. The first step is to clone the repo and install all required dependencies:
$ git pull https://github.com/chief-wizard/prisma-fastify-bakery.git
$ cd prisma-fastify-bakery
$ npm install
We’ll also need to make sure that we have a MySQL server running. If you need help installing and setting up MySQL, check out Prisma’s guide on the topic.
To document where the database can be reached, we’ll create a .env file in the root of our repository:
$ touch .env
Now, we can add the database URL to the .env file. Here’s how an example file looks:
DATABASE_URL='mysql://root:bakingbread@localhost/mydb?schema=public'
With the setup completed, let’s move on to the step where we create our Prisma schema.
Creating a Schema
The first step on our way to type safety is to add a schema. In our prisma/schema.prisma file, we define the data source, which in this case is our MySQL database. Note that rather than hard-coding our database credentials in our schema file, we read the database URL from a .env file. Reading in sensitive data from the environment is safer in terms of security:
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
We then define the types that are relevant for our application. In our case, let’s look at the model for the products that we’ll sell at the bakery. We want to record items like baguettes and croissants, and also make it possible to track items like bags of coffee and bottles of juice. Items will have types like “pastry,” “bread,” or “coffee”, and categories like “sweet” or “savory,” where applicable. We’ll also store references to sales for each product, as well as the products’ prices and ingredients.
In the Prisma schema file, we start by naming our Product model:
model Product {
...
}
We can add an id property—this will help us quickly identify each record in the products table and will serve as the index:
model Product {
...
id Int @id @default(autoincrement())
...
}
We can then add all the other properties that we’d like each item to contain. Here, we’d like the name of each item to be unique, giving us only one entry for each product. To refer to ingredients and sales, we use Ingredient and Sale types, which we define separately:
model Product {
...
name String @unique
type String
category String
ingredients Ingredient[]
sales Sale[]
price Float
...
}
We now have a complete model for our products in our Prisma schema. Here is how the prisma.schema file looks like, including the Ingredient and Sale models:
model Product {
id Int @id @default(autoincrement())
name String @unique
type String
category String
ingredients Ingredient[]
sales Sale[]
price Float
}
model Ingredient {
id Int @id @default(autoincrement())
name String @unique
allergen Boolean
vegan Boolean
vegetarian Boolean
products Product? @relation(fields: [products_id], references: [id])
products_id Int?
}
model Sale {
id Int @id @default(autoincrement())
date DateTime @default(now())
item Product? @relation(fields: [item_id], references: [id])
item_id Int?
}
To translate our model into live database tables, we instruct Prisma to run a migration. A migration contains the SQL code to create the tables, indices, and foreign keys in the database. We also pass in the desired name for this migration, init, which stands for “initial migration”:
$ npx prisma migrate dev --name init
We see the following output indicating that the database has been created in accordance with our schema:
MySQL database mydb created at localhost:3306
The following migration(s) have been applied:
migrations/
└─ 20210619135805_init/
└─ migration.sql
...
Your database is now in sync with your schema.
✔ Generated Prisma Client (2.25.0) to ./node_modules/@prisma/client in 468ms
At this point, we're ready to use the objects defined by our schema in our application.
Creating a REST API That Uses Our Prisma Schema
In this section, we’ll start to use the types from the Prisma schema and thus set the stage for implementing type safety. Jump straight to the next section if you’d like to see type-safety checks in action.
Since we’re using Fastify for our example, we create a product.js file under the fastify/routes directory. We add the products model from our Prisma schema, as follows:
const { PrismaClient } = require("@prisma/client")
const { products } = new PrismaClient()
We can then define a Fastify route that uses the Prisma-provided findMany function on the model. We pass the parameter take: 100 to the query to limit the results to a maximum of 100 items, to avoid overloading our API:
async function routes (fastify, options) {
fastify.get('/products', async (req, res) => {
const list = await product.findMany({
take: 100,
})
res.send(list)
})
...
}
The real value of type safety comes into play when we try to add a creation endpoint for our bakery products. Normally, we would need to check every input for its type. But in our example, we can skip the checks completely because Prisma Client will run them through the schema first:
...
// create
fastify.post('/product/create', async (req, res) => {
let addProduct = req.body;
const productExists = await product.findUnique({
where: {
name: addProduct.name
}
})
if(!productExists){
let newProduct = await product.create({
data: {
name: addProduct.name,
type: addProduct.type,
category: addProduct.category,
sales: addProduct.sales,
price: addProduct.price,
},
})
res.send(newProduct);
} else {
res.code(400).send({message: 'record already exists'})
}
})
...
In the example above, we go through the following steps in the /product/create endpoint:
Assign the body of the request to the variable addProduct. The variable contains all the details that were supplied in the request.
Use the findUnique function to find out whether we already have a product with the same name. The where clause allows us to filter the results to only include products with the name that we have supplied. If productExists variable is non-empty after running this query, then we already have an existing product with the same name.
If the product does not exist:
We create it with all the fields that were received in the requests. We do so by using the product.create function where the details of the new product are located under the data section.
If the product already exists, we return an error.
As the next step, let’s test the /product and /product/create endpoints using cURL.
Populating the Database Using Prisma Studio and Testing Our API
We can start our development server by running the following command:
$ npm run dev
Let’s open Prisma Studio and have a look at what’s currently inside our database. We’ll run the following command to launch Prisma Studio:
$ npx prisma studio
Once it’s launched, we’ll see the different models we have in our application and the number of records each one has over at a local URL http://localhost:5555:
There are currently no entries under the Product model, so let's create a couple of records by clicking the "Add new record" button:
With these data points added, let’s test our products endpoint using the following cURL command:
$ curl localhost:3000/products
# output
[{"id":1,"name":"baguette","type":"savory","category":"bread","price":3,"ingredients":[]},{"id":2,"name":"white bread roll","type":"savory","category":"bread","price":2,"ingredients":[]}]
Easy enough! Let’s create another product through our product creation API:
$ curl -X POST -H 'Content-Type: application/json' -d '{"name": "rye bread roll", "type":"savory", "category":"bread", "price": 2}' localhost:3000/product/create
# output
{"id":3,"name":"rye bread roll","type":"savory","category":"bread","price":2,"ingredients":[]}
Another item successfully created! Next, let’s see how our example does when it comes to type safety.
Trying Out Type Safety in Our API
Remember that we’re currently not checking the contents of the request on our product creation endpoint. What happens if we incorrectly specify the price using a string instead of a floating-point number? Let’s find out:
$ curl -X POST -H 'Content-Type: application/json' -d '{"name": "whole wheat bread roll", "type":"savory", "category":"bread", "price": "1.50"}' localhost:3000/product/create
# output
{"statusCode":500,"error":"Internal Server Error","message":"\nInvalid `prisma.product.create()` invocation:\n\n{\n data: {\n name: 'whole wheat bread roll',\n type: 'savory',\n category: 'bread',\n sales: undefined,\n price: '1.50',\n ~~~~~~\n ingredients: {\n connect: undefined\n }\n },\n include: {\n ingredients: true\n }\n}\n\nArgument price: Got invalid value '1.50' on prisma.createOneProduct. Provided String, expected Float.\n\n"}
As you can see, the Prisma checks have prevented us from creating an item with incorrect pricing—without our having to add any explicit checks for this particular case!
Tips for Adding Type Safety for an Existing Project
By this point, we’re well aware of the value that type checks can add to a JavaScript project. If you’d like to try adding such checks to your existing project, here are a few tips to help you get started.
Introspect a Database To Generate an Initial Schema
When using Prisma, database introspection allows you to look at the current layout of tables in a database and generate a fresh schema from the information you already have. This feature is a helpful starting point if you don’t feel like writing the schema by hand.
Try running npx prisma introspect, and a new schema.prisma file will be automatically generated in your project directory in only a few seconds.
Type Checks in VS Code
If Visual Studio Code is your programming environment of choice, you can take advantage of the ts-check directive to get type-checking suggestions directly in your code. In your JavaScript files that use the Prisma Client, add the following comment at the top of each file:
// @ts-check
With this check enabled, if we try to make a type error, we’ll get an immediate warning from VS Code:
this Productive Development with Prisma article.
The highlighting of type errors makes it easier to spot type-related issues early on. Learn more about this functionality inType Checks in Your Continuous Integration Environment
The trick above with @ts-check works because Visual Studio Code runs your JavaScript file through the TypeScript compiler. You can also run the TypeScript compiler directly, e.g., in your continuous integration environment. Adding type checks on a file-by-file basis can be a viable way to kickstart your type safety efforts.
To start checking types in a file, add the TypeScript compiler as a development dependency:
$ npm install typescript --save-dev
With the dependency installed, you can now run the compiler on JavaScript files, and the compiler will issue warnings if anything seems off. We recommend running TypeScript checks on one or a few files to start:
$ npx tsc --noEmit --allowJs --checkJs fastify/routes/product.js
Learn More About Implementing Type Safety in JavaScript
Ready to bake some type-safe code into your own codebase? Check out our complete code example in the prisma-fastify-bakery repository and try running the project yourself.
If you get stuck, Prisma has a helpful Slack community that you can join. It’s a great place to ask questions if you need help along the way.
Opinions expressed by DZone contributors are their own.
Comments