Welcome to the second post in our series about TypeScript generics! In this post, we’ll take a closer look at TypeScript inference. We’ll discuss topics like advanced inference, mapped, and conditional types.
If you haven’t already, you should read the first part of the series about the basics of generics to follow along better.
Advanced inference in TypeScript
By combining generics with type inference, we can create advanced types built on top of each other. By creating new types in this way, you can create a robust type system for your project that is easy to maintain because all the children types will automatically adopt the change in their parents.
You may find the syntax for advanced type inference a bit tricky at first, but with a little practice, you should be able to grasp it.
Let’s start with the mapped types.
Mapped types
Mapped types derive from other types. When we combine mapped types with generics, we can form powerful and flexible type definitions.
Creating new types using the keyof
is one example. In
the first post
, we briefly touched on the keyof
. Now let’s explore it more in-depth.
keyof
is a TypeScript operator that lets you derive new types based on the properties of others. It takes an input type and returns a new one with all of the properties of the original.
Here’s an example:
|
|
We use keyof
in a generic type KeysOfObject
that we can use to create other mapped types. We use KeysOfObject
to create a new UserKeys
type based on the properties of User
.
We get an error on line accessUser(user, "SSN")
. The reason for this is that we used the type UserKeys
to make sure our function only accepts existing user fields.
Creating mapped types is such a fundamental feature that TypeScript added utility types to the language. These utility types like Partial, Readonly, Pick and others make it easier to create custom mapped types.
These utility types use generics under the hood to work with the types they receive as the input. Let’s cover these three utility types in more detail.
Partial
Partial is used to create a new type with all of the fields set to optional. It’s great for working with an incomplete instance of the object where you don’t know ahead of time which fields may be missing:
|
|
Since we’re setting our body
param to type Partial<User>
, we don’t have to provide the whole user object to this function, only the fields we’re trying to update.
Readonly
The Readonly
utility type is used to create a new type with all of the fields set to read-only. TypeScript won’t allow you to change any field value on such objects after the initialization:
|
|
Here, we’re creating a new type Readonly<User>
, with the same fields as the User type, except with read-only permissions. When we create a new instance of the readonlyUser
and try modifying it, TypeScript throws an error.
Pick
The Pick
utility is used for creating a new type by indicating which fields you wish to copy. To choose the fields, you pass them as a union type:
|
|
See my other post on the most useful utility types for React to learn more about utility types and how they can help you.
Conditional types
Now let’s cover conditional types. We know about conditional expressions in JavaScript:
|
|
Turns out, we can use the same syntax to define types. TypeScript’s conditional types allow us to return a different type based on the value of the input type we receive:
|
|
The code above defines a conditional type, BarkOrMeow<T>
, which returns a type with either barkSound
or meowSound
depending on the input T
. Then we create a CatSound
type by passing the Cat
type to BarkOrMeow
.
Even with this trivial example, you can see how useful TypeScript conditional types can be.
Distributed conditional types
When defining conditional types, instead of returning a single type as part of our conditional statement, we can return several distributed types.
Distributed types allow you to add another level of flexibility to your code and handle more complex edge cases:
|
|
In the example above, we’re using distributed conditional type dateOrNumberOrString
to enforce the type for the second parameter of our compareValues
function. If the value1
is a Date
, we want value2
to also be a Date
. If value1
is a number, we want value2
to be either Date or a number.
Conditional type inference
A more complex case for conditional types is inferring the new type as a part of the conditional statement. We can use the infer
keyword to infer the new type based on a certain property or signature of the input we receive.
This might sound a bit too abstract without a concrete example, so let’s take a look at one:
|
|
In this example, we infer type U
from the id
field of type T
. If T
has the id
property, TypeScript infers the type of that property as U
. You can then use U
within the same conditional type statement.
Conditional type inference allows creating a robust type checking logic that can deal with deeply nested objects.
Type inference from function signatures
In the same fashion that we infer types from object fields, we can also infer types from function signatures.
We can infer types from function parameters as well as from function return types:
|
|
Here we have two generic types that are inferring types from input function types. The first one uses the function parameters and the other function’s return type.
Let’s take a look at how you would use one of these types in action:
|
|
We have executeFunction
function that uses inferFromFunctionParam
generic type we created earlier on its second argument arg
.
The first argument of executeFunction
is a function that takes one parameter, param
. TypeScript will infer the type of the second argument arg
from the type of param
.
In other words, if fn
is supposed to receive a string, TypeScript ensures we can only pass a string as a value for the second arg
parameter of executefunction
.
If we tried to call executeFunction
with a number, TypeScript would throw an error:
|
|
The ability to enforce types based on the function signature of function parameters you receive allows for a whole other level of type safety.
Conclusion
In this post, we covered advanced type inference and combined it with generics to build flexible types on top of other ones. With a strong grasp on generics and type inference, we can ensure all of the data that flows through our app has strong type safety.
And that concludes our deep dive into TypeScript generics. Again if you haven’t read the first part yet, I encourage you to do so as it lays the foundation for understanding TypeScript generics .
If you’d like to get more web development, React and TypeScript tips consider
following me on Twitter,
where I share things as I learn them.
Happy Typescripting!