Conditional + mapped types cheatsheet.
Conditional basics
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<42>; // false
infer
type Return<T> = T extends (...a: any[]) => infer R ? R : never;
type Arg0<T> = T extends (a: infer A, ...rest: any[]) => any ? A : never;
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type Elem<T> = T extends (infer E)[] ? E : never;
Distribution over unions
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]
Naked type param distributes. Wrap with [T] to prevent:
type ToArrayWide<T> = [T] extends [any] ? T[] : never;
type R = ToArrayWide<string | number>; // (string | number)[]
Filter unions
type Filter<T, U> = T extends U ? T : never;
type Nums = Filter<string | number | boolean, number>; // number
(That’s basically Extract.)
Mapped types
type Partial2<T> = { [K in keyof T]?: T[K] };
type Readonly2<T> = { readonly [K in keyof T]: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };
Remove modifiers
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Required2<T> = { [K in keyof T]-?: T[K] };
-readonly / -? strip the modifier.
Key remapping (as)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
type G = Getters<{ id: number; name: string }>;
// { getId: () => number; getName: () => string }
Filter keys via as … never
type StringFields<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
};
type S = StringFields<{ a: string; b: number; c: string }>;
// { a: string; c: string }
Mapped + conditional combo
type Promisify<T> = {
[K in keyof T]: T[K] extends (...a: infer A) => infer R
? (...a: A) => Promise<R>
: T[K]
};
DeepPartial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
Paths into nested object
type Path<T> = T extends object
? { [K in keyof T & string]: `${K}` | `${K}.${Path<T[K]> & string}` }[keyof T & string]
: never;
type P = Path<{ a: { b: { c: number } } }>;
// "a" | "a.b" | "a.b.c"
Use carefully — recursive types can blow up.
ValueAt path
type ValueAt<T, P extends string> =
P extends `${infer K}.${infer Rest}`
? K extends keyof T ? ValueAt<T[K], Rest> : never
: P extends keyof T ? T[P] : never;
Template literal manipulation
type SnakeToCamel<S extends string> =
S extends `${infer L}_${infer R}`
? `${L}${Capitalize<SnakeToCamel<R>>}`
: S;
type X = SnakeToCamel<"hello_world_foo">; // "helloWorldFoo"
Length of tuple
type Len<T extends readonly any[]> = T["length"];
type L = Len<[1, 2, 3]>; // 3
Head / Tail
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer R] ? R : [];
Tuple to union
type T2U<T extends readonly any[]> = T[number];
type U = T2U<["a", "b", "c"]>; // "a" | "b" | "c"
Union to tuple (use sparingly)
type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
type LastOf<U> =
UnionToIntersection<U extends any ? () => U : never> extends () => infer R ? R : never;
type U2T<U, L = LastOf<U>> = [U] extends [never] ? [] : [...U2T<Exclude<U, L>>, L];
Fragile. Often a sign of bad design — but useful sometimes.
Recursive depth limits
TS limits recursive types to depth ~50. Beyond that, you’ll hit “type instantiation is excessively deep.”
When to stop
If a type is unreadable, switch strategies:
- runtime check + assertion function
- code generation
- accept a wider type
Type-level magic costs maintenance.
Common mistakes
- Distribution surprise —
T extends X ? Y : neverover a union. - Mutual recursion without depth guard — TS bails.
- Forgetting to constrain
infer— usually fine, but matters for narrowing. & stringonkeyof T— needed when keys can be symbols.
Read this next
If you want my advanced type utilities, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .