Mapped & Conditional Types: How we turn one type into many
Mapped types and conditional types are the 'magic' behind utilities like Partial, Pick, Awaited, and more. In this post we’ll build them from first principles and see how they let us turn one type into many.
Many of us how either used, encountered or at least come across utility types in TypeScript, those such as Partial<T>, Pick<T>, ReturnType<T>, Awaited<T>, etc etc.
The thing that powers those utilties? Mapped and conditional types.
So how does this all work?
Well, let’s find out together.
Why would we want to turn one type into many?
type User = {
id: string;
email: string;
name?: string;
createdAt: Date;
updatedAt: Date;
};
So here we have a basic domain type, our User, which we either created manually or, more likely, generated from some API schema.
Now in a typical application we many need many different versions of this User:
- When creating a user we don’t want to provide an id or the timestamps.
- When updating a user all fields should be optional
- Maybe we want to display the user information on a public profile? So no email shown.
- We have an instance where the user might be readonly so we can’t accidently mutate.
We could manually create all of those types, but imagine what this would be like in a real application where we may have lots of domain types? Are we going to manually create types for them all? We’d also need to keep all the types we created in sync if something changed.
I think we can all agree, it’d be better if we didn’t have to do this.
The ability to turn this one type into many will save us a lot of time and prevent endless headaches.
Mapped Types: Changing modifiers
type Mapped = {
[K in SomeKeyUnion]: SomeType;
};
A mapped type reads as:
For every key, K, in some set of keys, SomeKeyUnion, produce a property, SomeType.
This will be easier to visualise in an example, so we can start by recreating the Partial utility.
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
Now while this may look confusing we can break it down into steps:
keyof T- Creates a union of keys for the typeT[K in keyof T]- Loops over every keyK?:- Marks each key as optionalT[K]- Looks up the property type fromT
So if we used it like so:
type User = {
id: string;
email: string;
name?: string;
createdAt: Date;
updatedAt: Date;
};
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
type UserPatch = MyPartial<User>;
The following will happen:
- A union is created of the keys in
User, so:keyof User="id" | "email" | "name" | "createdAt" | "updatedAt" - As we loop over each key,
Kis assigned that key - We add the
?:to the key, making it optional. - We then assign the key back to the original property type from
User
So we end up with:
type UserPatchManual = {
id?: string;
email?: string;
name?: string;
createdAt?: Date;
updatedAt?: Date;
};
Adding and removing modifiers
In the last section we used a mapped type to add the optional modifier to all the keys of the User type.
We’re not limited to just making them optional, infact we’re not limited to just adding modifiers either. We can remove them.
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
Above is our own version of the ReadOnly<T> utility type. Like the MyPartial example above, this adds the readonly modifier to all keys of a provided type.
type MyRequired<T> = {
[K in keyof T]-?: T[K]; // remove optional flag, ?, by using -?
};
Now we have our own version of the Required<T> utility type. This does the opposite of MyPartial, it removes the optional modifier from every key in the type.
type MyMutable<T> = {
-readonly [K in keyof T]: T[K]; // remove readonly
};
There are no built in utility type for this but it simply does the opposite of MyReadonly, it removes readonly from all keys in the provided type.
Mapped types: Filtering and renaming properties
In the previous section the mapped types kept keys the same and only changed their modifiers.
But mapped types allow us to do much more, we can filter out particular keys and even rename them.
How? We use the as operator.
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
Breaking this down:
K in keyof T- Like before will convert the typeT’s keys into a union and then loop over them.as- Will decide what the new key will be based on some conditionalT[K] extends string ? K : never- The conditional used byasto determine the keyT[K]- Looks up the property type fromT
So the main difference here is the as and the conditional, lets break down the conditional so we understand what is happening.
We check if T[K] extends string, which means we look at the propery type from T and check if it is a string. If it is a string we keep the key as K otherwise we set the key to never.
Setting the key to never removes this property from the type.
Looking at an example:
type Example = {
id: string;
count: number;
label: string;
};
type StringProps = OnlyStrings<Example>;
// {
// id: string;
// label: string;
// }
The count property is remove because its type was not string.
Building a Create type
Earlier we discussed how we may want to create a type for handling the creating of domain types, like users.
We now have all the tools necessary to build this type!
type Create<T> = {
[K in keyof T as K extends "id" | "createdAt" | "updatedAt"
? never
: K]-?: T[K];
};
So breaking this down now:
K in keyof T- Convert the typeT’s keys into a union and then loop over them.as K extends "id" | "createdAt" | "updatedAt"- Check to see if the key,K, equals"id"or"createdAt"or"updatedAt"? never- Assign toneverif it does equal one of the values, removing it from the type.: K]-?- Otherwise assign the key back and remove any optional modifiers.T[K]- Looks up the property type fromT
We now has a generic Create type that can be used with any of our domain types. This is a common pattern when building web backends.
Here we brought together mapped and conditional types to create reusable generic type.
Next we’re going to focus more on the conditional side of things.
Conditional Types: T extends U ? X : Y
In the previous section we breifly touched on this but we will explore it in more detail now.
So what is T extends U ? X : Y?
Well in plain English:
If T is assignable to U then produce X otherwise produce Y
They behave exactly the same way as conditionals do in JavaScript, where we use the ternary operator.
Lets look at a simple example:
type IfString<T> = T extends string ? "yes" : "no";
type A = IfString<string>; // "yes"
type B = IfString<number>; // "no"
We can read this as simply as if the provided type, T is a string then produce "yes" otherwise produce "no".
We can create some basic utilities with conditionals:
type IsPromise<T> = T extends Promise<any> ? true : false;
type X = IsPromise<Promise<number>>; // true
type Y = IsPromise<number>; // false
Simply checks is a provided type is a promise.
Conditionals are good on their own but their true power is unlocked when combined with other TypeScript features like mapped types above.
Or we can combine conditionals with the infer keyword…
Let TypeScript figure it out with infer
A classic example of this is:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
Breaking this down:
T extends Promise<infer U>- Check to see ifTlooks like Promiseinfer U- Capture what the ‘Something’ as a new type variableU? U : T- IfTis a promise return the inner type,U, otherwise returnT
So really infer allows us to create a new type variable from within this pattern by letting TypeScript work out the type.
The above is a simplified version of the built in Awaited<T> in TypeScript. Awaited<T> handles more edge-cases, like nested promises and Thenable, but it conceptually is the same:
If this type looks like a promise then give me the type inside the promise