TypeScript Utility types with Generics
permalinkThis one will be a little bit more advanced, as we'll be looking at improving our combined utility type we made the other day.
The code so far looks like this:
interface User {
id?: number;
firstname: string;
lastname: string;
age?: number;
}
type LoggedUser = Required<Pick<User, 'id'>> & Omit<User, 'id'>;
const u1:LoggedUser = {
id: 1,
firstname: 'Chris',
lastname: 'Bongers'
}
The LoggedUser
type is a modified version of the User
interface, requiring specific fields.
In our case, we make the id
field required.
However, this kind of action of requiring fields might become a feature we would like to re-use throughout our application.
And by looking at generics types, it's exactly what we can use to make this happen.
Making a generic require fields utility type permalink
We would love to have a RequireFields
type. This type could then be used to define a list of required fields for a specific type.
The great part about types is that we can define information in their generics section like so:
type RequireFields<Type>
The Type
will now be available to work with inside the function.
Let's take a step back and see what details we need.
type LoggedUser = Required<Pick<User, 'id'>> & Omit<User, 'id'>;
By looking at the above, we see that we need the User
type and the field we want to require, the id
.
When we looked at generics types, I briefly mentioned there is not a limit to one type so that we can pass multiple types like this:
type RequireFields<Type1, Type2>
The first one in our case will be User
, which we can define as T
. However, the second one is a bit different since it can contain one or multiple keys from this T
(User).
Luckily for us, TypeScript has a feature that does just that.
The function looks like this:
K extends keyof T
Here we define K
as our second type, and K
should act as an extended key object of the T
.
Let's quickly look at what this could return to see what we are working with.
As you can see in the image above, the keys for this interface are: "id" | "firstname" | "lastname" | "age"
.
By using extends keyof Type
, we make sure we can only pass keys that are part of the object.
Looping back to our RequireFields
type, we can set the generic types to be the following:
type RequireFields<T, K extends keyof T>
In this case, the T
will be our type, and the K
will be the keys of this type we want to use.
Note: Type keys can be one or multiple delimited by an
|
.
Then we can modify what we had before to work with these two generic types.
Before:
type LoggedUser = Required<Pick<User, 'id'>> & Omit<User, 'id'>;
After:
type RequireFields<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
We can simply call this RequireFields
type and pass the type and keys we want to require.
const u2:RequireFields<User, 'id' | 'age'> = {
id: 2,
age: 32,
firstname: 'Chris',
lastname: 'Bongers'
}
Remember when I said the extends keyof
will check for the right keys? Let's try and modify the age
key to a key that doesn't exist.
In this image, you can quickly see TypeScript will warn us that this email
field does not exist on the User
type.
Conclusion permalink
This is quite a complex concept to grasp at first, and I urge you to try it out yourself.
By playing around and following the steps, you should understand what this code does in detail.
These generic types combined with the utility types make TypeScript super exciting and versatile.
Thank you for reading, and let's connect! permalink
Thank you for reading my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter