In this guide, we'll look at some of TypeScript's most important features and how you can use them to your advantage.
- Type Inference
- Union Types
- Intersection Types
- Type Assertions
- Reserved Type Names and Keywords
- Aside: Type-Safe Database Access with Prisma
Types are the heart of TypeScript. Types give our code editors insights about our code. They inform us about the ways our code is either valid or invalid well before we even run it.
To take advantage of the type system, we need to provide it some information about our code. At the simplest, this involves using type annotations.
To apply a type annotation to a variable, we put a colon after the variable name followed by the type.
In this example, we're telling the type system that
companyName has a type of
string. If we were to later try to assign something to
companyName other than a
string, we would get a type error.
In this case, type-hinting our variable has prevented us from misusing it. This is a trivial example, but the benefits are clear. As codebases and teams grow and become more disparate, the type system becomes invaluable as a way to ensure code is used in the way it should be.
In the example above, we applied a type to a variable. There are, however, many other places in a codebase that we might want to apply types.
The most common spots we might see types applied include:
- Function parameters
- Function returns
Let's consider the following function for getting the length of a string:
We're applying types to all three of the spots listed above. The
word parameter is typed as a
string, meaning that we'll get a type error if we try to pass in anything but a string. Type-hinting this parameter is done much like how we type-hint a variable. We put a colon next to the parameter followed by the type.
The same kind of thing can be done after the closing parenthesis. In this case, we know that the function should return a
number (the length of the word passed in), so we type hint the function return to
Finally, the variable we're declaring called
wordLength is typed as a
number as well. That's because we know the return type of
getWordLength is number. Setting the
wordLength type to anything else (besides
any) will result in a type error.
We can see the type system at work if try to use this function in a way that isn't valid. For example, if we try to pass a number as the argument, we are immediately stopped with a type error.
We can also see issues if we make changes to the body of the function that throws off the return type. For example, if we try to return a string instead of a number, we'll get a type error.
In both cases, TypeScript is protecting us from code misuse.
TypeScript does a good job of inferring type information from the code we write. There are many cases where we don't explicitly need to tell the compiler what type something should have. Instead, the type information can instead be safely assumed by TypeScript.
Let's take the example of the
getWordLength function from above.
When we set a variable to hold a value of
42 and then try to pass that variable to the
getWordLength function, we get a type error. This makes sense since
42 is a
number and the function expects a
string. But we didn't explicitly set the variable
word to be of type
number, so how is this being caught?
It's because TypeScript can see that the value assigned to
word is indeed a number. Without explicitly telling the compiler about the type, our mistake is caught before our code runs.
Type inference is a great feature of TypeScript that can help us out with little effort on our part. It does, however, sometimes need to be worked around with type assertions which we'll look at a bit later.
There are many cases where assigning a single type to parts of our code would be too inflexible. For example, we might have a function that should accept either a string or a number. The function itself would then be responsible for properly handling both data types.
Let's take the example of a function that is responsible for converting a number value into a formatted currency string. If we were to just accept
number types as input, this would be fairly straight-forward.
This function gives us strong type safety for its input and output.
However, what if we wanted to also accept
string types as well? Since number values are sometimes stored as strings (for a variety of reasons), it would be ideal if we could make this function more flexible.
To do so, we can use a union literal with the pipe character
The union literal is a bit like the OR operator (
We can allow the
formatAsCurrency function to accept a
number or a
string as input but we'll need to be sure to do some work in the function itself to handle these types appropriately.
Now that the function accepts either a
number or a
string, we need to be careful that any strings passed in are actually representable as a number. If someone were to pass in a string with letters instead of just numbers, the result of
parseFloat would be
NaN which is almost certainly not what we want.
Running a check on the argument type is a good way to get us on a path where we can do the appropriate checks for strings validity.
Intersection types are useful for composing together two or more different types into a single type.
Let's say we have a
User type and an associated
Let's also say we have a function for accessing this data. However, instead of simply returning the
user directly, we want to add an additional
role property to it.
To make our function type-safe, we should apply a return type to it. But how should we go about doing so? Our
User type lacks the
role property that we're returning with the
We could add
role to the
User type, but this may not be what we want. In fact, there are many instances where that would be a deal-breaker as it would throw off our type hints in other places. In other words, there are legitimate cases where the
User type should not be touched.
We might then think about creating a new type which would represent the data as we want to.
This approach works but we're creating a whole new type that is quite specific to this one task. We may not be able to reuse it in other places.
A more flexible approach is to use intersection types. This allows us to use different types together which means we can define types that are more generalized and reusable.
An intersection is done by using
& in-between multiple types. We can think of it a little bit like the
We aren't limited to just two types for intersections. We can intersect together as many types as we like.
TypeScript Generics allow us to write code that can work with a variety of different types, depending on the need. Instead of hard-coding type information, we can write our code such that the type information is applied when the code is called.
Let's say we have a function that returns an array of the first three items from an array passed in as an argument.
We give the
getFirstThree function an array of
items and it gives us back the first three.
Since we want to be type-safe with this code, we should apply type hints. We're dealing with strings here so we can tell the function that it should expect strings as input and give back strings as output.
Our function now expects the
items argument to be an array of strings and that it should also return an array of strings.
This works great for arrays of strings, but what if we want to also use it for an array of numbers?
We could make two separate functions to deal with these two cases.
It's a shame that we're duplicating the function just to suit the two different type-safety cases. Also, what happens if we wanted the function to also work for other types of arrays?
This is a great use case for generics.
Generics are a feature found in many type-safe languages that allow us to make type-hinting flexible. In our examples above, we have "hard-coded" the type that the function expects. To make it more adaptable to different inputs, we can make the type information for this function generic.
With generics, we can define our function with a "placeholder" type. Most often, this is done with a single character:
T. However, the character can be anything we want.
Here's what our function looks like with a generic type:
The generic type
T is a placeholder for a type that we supply when we call the function. It's like a parameter for us to supply an argument to.
Since we're passing
string as the type to apply to this function at call-time, the
getFirstThree function expects an array of strings as the
items argument and also expects to return an array of strings as well. If we pass an array of mixed types, we'll get a type error.
We can now reuse this function with other types, including
TypeScript is great at catching our mistakes before we even run our code. A common refrain from TypeScript developers is that getting past the type checker can sometimes be frustrating and require a lot of time but that it's well worth it.
There are, however, cases where our TypeScript code won't compile because it doesn't know as much as we do about our code. There are legitimate cases where we need to override the way TypeScript checks for types so that type checking can happen properly. For these cases, we can use type assertions.
A type assertion is an instruction we give to the TypeScript compiler for it to side-step its default behavior.
Let's say we have two types:
Contact and a variable that is type-hinted as
Let's also say that somewhere later in our code we have a function that is responsbile for returning an object shaped as a
Contact. We know that we want the result of this function call to be shaped as a
Contact because we need to access certain properties such as
phone later on.
If we tried to return
person from this function, we'd get a type error.
We could adjust the
Contact type to say that
phone are nullable and then include these properties in the funtion return:
This case is rather trivial. For real-world cases where our data and code get more complex, this approach may not be feasible.
To get around this, we can tell the TypeScript compiler to treat
person as a
Contact just this one time. To do so, we assert that
person is a
The syntax here is fairly straight-forward: we use the
as keyword such that the thing on the left should be "treated as" the thing on the right.
The result of doing this is that we'll get
phone and thus this operation is unsound.
However, this might be fine for our needs. Perhaps we're rendering a list of contacts and they happen to not have an
phone defined, we just deal with that in the render.
number to a function when it really should be passed a
string), TypeScript can be used to remove a whole class of errors before they even occur.
There are, however, times when we don't know (or can't know) what type something should have. For these cases, we can use the
any type works a lot like you'd expect from its name. Type-hinting something as
any means that it becomes usable anywhere.
This can be useful if we can't know the type for something. It should, however, be used sparingly. It often becomes an escape hatch for developers if they don't want to spend time applying the appropriate type to something but this also means that we lose the benefits of type safety.
Let's consider a function called
addOne which takes in a number and adds
1 to it.
If we were to declare a variable that holds a string that should be passed to
addOne, type inference would prevent us from doing so.
However, if we were to apply
any as the type for the
companyName variable, we would get passed the type checker.
The result from the function here is obviously not what we intend and is indeed a cause for concern since it produces a bug.
any type should be used sparingly, if at all. It negates the purpose of TypeScript and will lead to moore brittle code.
unknown type is similar to
any in some ways but with a key distinction: the
unknown type can only be assigned to itself and to the
any type whereas
any can be assigned to anything. You can, however, assign any value to something with a type of
unknown type would make a good alternative for our contrived example above where we don't want to type-hint
companyName with its proper type.
In this example, we are able to assign a string value to a variable with type
unknown, but we are not able to use that variable in a function that expects a
The value of the
unknown type is that it gives us an escape hatch for cases where we don't or can't know the type of something while still being type-safe.
Type safety is invaluable for catching bugs before our code runs. Having a type-safe codebase means we can making sweeping changes and refactor our code with confidence.
Type safety is also of great benefit when it comes to database access.
Instead of writing raw SQL statements, it's beneficial to use an ORM (Object Relational Mapper) to query our database. What's even better is if this ORM is type-safe.
Prisma is a next-generation ORM and database toolkit for TypeScript and Node.js which makes it easy to apply type-safety to our databases. We can start with a simple database model and get an automatically-generated type-safe client to access our databases in minutes.
Let's see how to wire up Prisma in a Node.js project.
Assuming you already have a TypeScript-based Node.js project, you can get started by installing the Prisma CLI.
The CLI is installed as a dev dependency since we won't need it for production.
With the Prisma CLI insatlled, initialize Prisma in your project.
This will create a
prisma directory in your project. Inside, you'll find a
schema.prisma file. This is where you define the model for your database.
Let's start with a simple SQLite database and table to hold some blog posts.
In this schema, we're telling Prisma that we want to use SQLite as our database. SQLite is a filesystem-based database that may not be suitable for production but is great for development.
Note: Prisma currently supports PostgreSQL, MySQL, MS SQL Server, and SQLite.
We're defining a model called
Post and giving it three fields:
body. This model will produce a table called
Post in your database with these fields and will take the type information that you provide on the right side of the field name as their data types.
id field has a type of
Int which will map to the
INTEGER type in SQLite. Likewise for the
String type on
body, the database will get a
TEXT type. The
id field is also defaulting to an autoincremented value. This means that each subsequent record will take on a value that is one higher than the last.
With the model in place, run the command to create the database and wire up the table.
prisma db push command does two things: it creates the
dev.db database in the
prisma directory and it also creates the
Post table within.
You can see this database and table working with Prisma studio.
Prisma Studio is a fully-featured database client that is useful for debugging and development.
With the database and table in place, install Prisma Client and generate it to get access to the database.
After installing the
@prisma/client package, the Prisma Client will automatically be generated using
prisma generate. This command is responsible for looking into the Prisma schema file and creating TypeScript types which allow for type-safe database access.
In a TypeScript file within your Node.js project, import the Prisma Client and create an instance.
Post type that is imported from
@prisma/client was generated when the
npx prisma generate ran. It can now be used in various places across the application. In this case, it's being applied as the type for the
Promise that the
createPost function returns.
The value of a type-safe database client can be seen if you try to input something to the
Post table that shouldn't be there. For example, if you try to include an author name, a type error would be raised:
Since mistakes like this are caught before the code runs, it becomes difficult to make mistakes when it comes to data input. This is very helpful because it eliminates a whole class of potential bugs before the code is even shipped.
If you have any questions on how TypeScript can fit into your project or how you can benefit from using Prisma for type-safe database access, please feel free to reach out to us on Twitter.
Don’t miss the next post!
Sign up for the Prisma Newsletter