Mapped and conditional types
Types computed from other types — mapping over keys, renaming them, branching with extends and pulling pieces out with infer.
After this lesson you can
- Write a mapped type with modifiers and key remapping
- Write a conditional type and say when it distributes over a union
- Use infer to extract part of a type
- Pair a computed type with the runtime code that produces matching values
Utility types such as Partial and ReturnType are not built into the
compiler; they are written with the two features in this lesson.
Mapped types
A mapped type walks the keys of another type and produces a new property for each one:
type Partial<T> = { [K in keyof T]?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] }; // -? removes optionality
The value side can be anything computed from K and T[K]. Here every
field of a form becomes a boolean, "was it filled in?":
// For every key of T, a boolean: was that field filled in?type Filled<T> = { [K in keyof T]-?: boolean }; function filled<T extends object>(form: T): Filled<T> { const out = {} as Record<string, boolean>; for (const [key, value] of Object.entries(form)) { out[key] = value !== null && value !== undefined && value !== ""; } return out as Filled<T>;} interface SignUp { email: string; name: string | null; company?: string;} export function run(): Filled<SignUp> { const form: SignUp = { email: "nino@example.com", name: null, company: "" }; return filled(form);}The type only describes the result. The function still has to build it,
and nothing checks that the loop and the type agree — which is why a
mapped type almost always comes with a cast (as Filled<T>) at the end of
its implementation.
Renaming keys
An as clause inside a mapped type remaps each key, usually with a
template literal type. Capitalize is one of the built-in string helpers,
along with Uppercase, Lowercase and Uncapitalize:
// Key remapping: "name" becomes "getName", and each value becomes a function.type Getters<T> = { [K in keyof T & string as `get${Capitalize<K>}`]: () => T[K];}; function getters<T extends object>(obj: T): Getters<T> { const out: Record<string, () => unknown> = {}; for (const key of Object.keys(obj) as (keyof T & string)[]) { out[`get${key[0]!.toUpperCase()}${key.slice(1)}`] = () => obj[key]; } return out as Getters<T>;} export function run(): unknown[] { const g = getters({ name: "Nino", age: 31 }); // g.getName(): string and g.getAge(): number, both known to the compiler. return [Object.keys(g), g.getName(), g.getAge()];}Remapping a key to never drops it, which is how you filter properties by
their type.
Conditional types
T extends U ? X : Y picks X when T is assignable to U, otherwise
Y. Inside the true branch, infer names a piece of the matched type so
you can use it:
type ElementOf<T> = T extends readonly (infer E)[] ? E : T;
type A = ElementOf<string[]>; // string
type B = ElementOf<number>; // number
When T is a bare type parameter and you pass a union, the condition is
applied to each member separately and the results are unioned:
ElementOf<string[] | number> is string | number. That is called
distribution. Wrapping both sides in a tuple, [T] extends [U], turns
it off.
Again, the type and the code are two halves of one idea — the runtime check asks the same question the type does:
// A conditional type picks a branch; infer names a piece of the matched type.type ElementOf<T> = T extends readonly (infer E)[] ? E : T; // The runtime check mirrors the type: same question, same two branches.function firstOrSelf<T>(value: T): ElementOf<T> | undefined { return (Array.isArray(value) ? value[0] : value) as ElementOf<T> | undefined;} export function run(): unknown[] { const n = firstOrSelf([3, 4, 5]); // number | undefined const s = firstOrSelf("solo"); // string | undefined const none = firstOrSelf([] as string[]); return [n, s, String(none)];}Common mistakes
- Forgetting the implementation is unchecked. A mapped return type
with an
ascast compiles even if the loop builds something else; test the function, not just the type. - Surprise distribution. With
type IsString<T> = T extends string ? true : false,IsString<string | number>givesboolean(true for one member, false for the other), notfalse. Use[T] extends [string]when you mean the union as a whole. keyof Tincluding numbers and symbols. In a template literal, restrict to string keys withkeyof T & string, as the getters example does.- Unreadable type gymnastics. A recursive conditional type nobody can read is a maintenance cost; reach for it when it removes real duplication.
What interviewers ask
- How is
Partial<T>implemented?{ [K in keyof T]?: T[K] }— a mapped type over the keys with the optional modifier. - What does
inferdo? Inside a conditional type'sextendsclause it declares a type variable bound to whatever matched at that position, as inReturnType.
Exercise · Pro
snake_case keys to camelCase, typed and at runtime
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade