Last 30 Days
No notifications
This is where TypeScript's type system becomes a small functional language. Mapped types loop over keys to transform a shape; conditional types branch on a type; infer extracts pieces from another type. Together they power every utility type you've already used.
{ [K in keyof T]: … }A mapped type walks over the keys of T and produces a new shape:
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };That's it — that's all Partial and Readonly are.
+ and -You can add or remove ? and readonly:
type Required<T> = { [K in keyof T]-?: T[K] }; // strip optional
type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // strip readonlyasRename keys while you map:
type Getters<T> = {
[K in keyof T as get${Capitalize<string & K>}]: () => T[K];
};type User = { id: number; name: string };
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }
You can also filter keys by remapping to never:
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};T extends U ? X : YBranches at the type level:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<42>; // falseConditional types distribute over naked union type parameters:
type ToArray<T> = T extends any ? T[] : never;
type X = ToArray<string | number>;
// (string number)[] ? No → string[]
number[]That's surprising the first time. To disable distribution, wrap in tuples:
type ToArray2<T> = [T] extends [any] ? T[] : never;
type Y = ToArray2<string number>; // (string number)[]Filtering unions
type NonNull<T> = T extends null
undefined ? never : T;
type S = NonNull<string null
undefined>; // stringThat's literally how NonNullable works.
infer — pull a type outInside the extends clause you can introduce a new type variable with infer:
type ElementOf<A> = A extends Array<infer U> ? U : never;
type N = ElementOf<number[]>; // number
type S = ElementOf<string[]>; // stringtype ReturnType2<F> = F extends (...args: any[]) => infer R ? R : never;
type R = ReturnType2<() => Date>; // Date
type AwaitedT<T> = T extends Promise<infer U> ? U : T;
type X = AwaitedT<Promise<number>>; // number
Real-world: pull params, return types, promise contents, JSX prop types — all powered by infer.
The classic "deep readonly":
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};type Config = { db: { host: string }; flags: { debug: boolean } };
type Frozen = DeepReadonly<Config>;
Mutable counterpart:
type DeepMutable<T> = {
-readonly [K in keyof T]: T[K] extends object ? DeepMutable<T[K]> : T[K];
};Pick keys whose value extends a type:
type KeysOfType<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never; }[keyof T];; type R = "/api/users/1"; // assignable to Routetype StringKeys = KeysOfType<{ id: number; name: string; email: string }, string>; // "name"
"posts"}/${number}
"email" That
{...}[keyof T]trick is the pattern for "give me the keys whose value matches a predicate".Template literal types
Strings as types you can compose:
type Route =/api/${"users"type EventName<T extends string> =
on${Capitalize<T>}; type E = EventName<"click">; // "onClick"
Pair with mapped types for component prop generation:
type Listeners<T extends string> = {
[K in T as on${Capitalize<K>}]: (e: Event) => void;
};
type Btn = Listeners<"click" | "focus">;
// { onClick: (e: Event) => void; onFocus: (e: Event) => void }