AcademyTypeScript
Mapped and conditional types
How to write the utility types you have been importing: looping over keys, branching on a type, and pulling a type back out with infer.
4 min readUpdated 3 exercises
A loop, at the type level
A mapped type walks the keys of a type and produces a new one. That is all Partial is:
type Partial<T> = {
[K in keyof T]?: T[K];
};Read it as a for...in: for each key K in keyof T, make a property of type T[K], optional. Every one of the built-in utilities is this shape, which is why they stop being magic once you have written one.
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type Stringify<T> = {
[K in keyof T]: string;
};Modifiers can be added or removed, with + and -:
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type Concrete<T> = {
[K in keyof T]-?: T[K]; // this is Required<T>
};Renaming keys as you go
as inside a mapped type rewrites the key. With template literal types, that is how a getter interface gets generated:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }Map a key to never and it disappears — which is how you filter properties by their type:
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type S = OnlyStrings<{ id: string; age: number }>; // { id: string }A conditional type is a type-level ternary
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // falseextends here means "is assignable to", not inheritance. The useful ones combine it with a generic:
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<string[]>; // string
type B = ElementType<number>; // numberinfer names a type the compiler works out
infer U says: match this position, and call whatever is there U. It only appears inside the extends clause of a conditional type, and it is how every "unwrap" utility works:
type Awaited<T> = T extends Promise<infer U> ? U : T;
type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type Parameters<F> = F extends (...args: infer P) => any ? P : never;
type A = Awaited<Promise<string>>; // string
type B = ReturnType<() => { id: string }>; // { id: string }Once you can read those three, most of lib.es5.d.ts is readable.
The distribution rule, and how to switch it off
A conditional type over a naked type parameter distributes across a union — it runs once per member:
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>; // string[] | number[]
// not (string | number)[]This is usually what you want — it is how Exclude works — and occasionally exactly what you do not. Wrapping both sides in a tuple stops it:
type IsUnion<T> = [T] extends [string] ? true : false;
type A = IsUnion<string | number>; // false, checked as a whole| Want | Write |
|---|---|
| Loop over keys | { [K in keyof T]: ... } |
| Rename keys | [K in keyof T as NewKey] |
| Drop a key | map it to never |
| Branch on a type | T extends X ? A : B |
| Capture part of a type | infer U inside the extends |
| Stop distributing over a union | [T] extends [X] |
When to stop
Type-level programming is genuinely fun, and that is its danger. A type nobody on the team can read is worse than a small amount of duplication: the error messages it produces are unreadable too, and they land on whoever is unlucky enough to pass the wrong argument. Write the clever type when it removes a real class of bug — and when it does, write a test for it, because a type has behaviour like anything else.
Now practice it
2 challenges · 1 quizReading this page is the cheap half. These are the exercises that make you use it.
- Mapped Types — Key RemappingProKey remapping with as and template literals, which is the mapped-type feature that does the most work.ChallengeSolve · MediumChallengeSolve · Medium
- Conditional Types & InferProConditional types with infer, until unwrapping a Promise or a return type is something you can write cold.ChallengeSolve · MediumChallengeSolve · Medium
- Advanced Types · Conditional Types & InferConditional types and infer, including the distribution rule that surprises people on a union.QuizTake · HardQuizTake · Hard
Read next
Partial, Pick, Omit and the rest
Partial, Pick, Omit, Record, Required, Readonly and the rest — what each one is for, which one is unsafe, and how to stop re-declaring the same shape.
3 min
Deriving types with keyof and typeof
The three operators that read types out of things you already wrote, so a type never has to be maintained next to the value it describes.
3 min