TypeScript Advanced Types Cheat Sheet
Covers conditional types, mapped types with key remapping, template literal types, core type operators, and discriminated union narrowing.
Conditional Types
Branch type-level logic on a condition, and extract types with infer.
type IsString<T> = T extends string ? true : false;type A = IsString<'hi'>; // truetype B = IsString<42>; // false// Distributive conditional types over unionstype ToArray<T> = T extends any ? T[] : never;type C = ToArray<string | number>; // string[] | number[]// infer keyword extracts a type from within a conditionaltype ElementType<T> = T extends (infer U)[] ? U : T;type D = ElementType<number[]>; // number
Mapped Types
Transform every property of a type, including renaming keys.
type Readonly2<T> = { readonly [K in keyof T]: T[K] };type Partial2<T> = { [K in keyof T]?: T[K] };// Key remapping with 'as' (TS 4.1+)type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];};interface Person { name: string; age: number; }type PersonGetters = Getters<Person>;// { getName: () => string; getAge: () => number }// Filtering keys with 'never'type NonFunctionKeys<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
Template Literal Types
Build string literal types by composing other literal types.
type Direction = 'top' | 'bottom' | 'left' | 'right';type Margin = `margin-${Direction}`;// 'margin-top' | 'margin-bottom' | 'margin-left' | 'margin-right'type EventName<T extends string> = `on${Capitalize<T>}`;type ClickEvent = EventName<'click'>; // 'onClick'// Combined with mapped types for typed event handlerstype Handlers<T extends string> = { [K in T as EventName<K>]: () => void };
Key Type-Level Operators
Operators used to build and query types.
- keyof- Produces a union of an object type's keys: keyof {a:1,b:2} is 'a' | 'b'
- typeof- Extracts the static type of a value: type T = typeof someVar
- in (mapped types)- Iterates over a union of keys to build a new object type
- infer- Declares a type variable to be inferred inside a conditional type
- as (key remapping)- Renames keys inside a mapped type
- extends (constraint)- Constrains a generic parameter or drives conditional type branching
- & (intersection)- Combines multiple types into one with all members
- | (union)- Represents a value that could be one of several types
Discriminated Unions
Narrow a union safely using a shared literal 'kind' field.
interface Circle { kind: 'circle'; radius: number; }interface Square { kind: 'square'; side: number; }type Shape = Circle | Square;function area(shape: Shape): number { switch (shape.kind) { // narrows the union based on the discriminant case 'circle': return Math.PI * shape.radius ** 2; case 'square': return shape.side ** 2; default: const _exhaustive: never = shape; // compile error if a case is missed throw new Error('Unhandled shape'); }}
Recursive Conditional Types
Conditional types can call themselves to walk arbitrarily nested structures.
type DeepReadonly<T> = T extends (infer U)[] ? ReadonlyArray<DeepReadonly<U>> : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;interface Config { db: { host: string; ports: number[] }; }type ReadonlyConfig = DeepReadonly<Config>;// db and db.ports become deeply readonly// Recursive tuple flattening (TS 4.1+ recursion limit ~ a few hundred)type Flatten<T> = T extends [infer Head, ...infer Rest] ? Head extends unknown[] ? [...Flatten<Head>, ...Flatten<Rest>] : [Head, ...Flatten<Rest>] : [];type F = Flatten<[1, [2, 3], [4, [5, 6]]]>; // [1, 2, 3, 4, 5, 6]
infer in Multiple Positions
Use infer more than once to pull several type variables out of a single shape.
// Extract a function's parameter and return types simultaneouslytype Unpack<T> = T extends (...args: infer A) => infer R ? { args: A; ret: R } : never;type U = Unpack<(a: string, b: number) => boolean>;// { args: [string, number]; ret: boolean }// infer with constraints (TS 4.7+)type FirstIfString<T> = T extends [infer Head extends string, ...unknown[]] ? Head : never;type G = FirstIfString<['hello', 1, 2]>; // 'hello'// Extract Promise value, recursively unwrapping nested promisestype Awaited2<T> = T extends Promise<infer V> ? Awaited2<V> : T;type R2 = Awaited2<Promise<Promise<number>>>; // number
Nominal Typing via Branding
Simulate nominal (not just structural) types when two structurally-identical types must stay distinct.
declare const brand: unique symbol;type Brand<T, B> = T & { readonly [brand]: B };type UserId = Brand<string, 'UserId'>;type OrderId = Brand<string, 'OrderId'>;function toUserId(id: string): UserId { return id as UserId;}function getUser(id: UserId) { /* ... */ }const uid = toUserId('u_123');const oid = 'o_456' as OrderId;// getUser(oid); // Error - OrderId is not assignable to UserIdgetUser(uid); // OK
Parsing Strings with Template Literal Types
Combine template literal types with infer to destructure string literals at the type level.
type ParseRoute<T extends string> = T extends `${infer Segment}/${infer Rest}` ? Segment extends `:${infer Param}` ? { [K in Param]: string } & ParseRoute<Rest> : ParseRoute<Rest> : T extends `:${infer Param}` ? { [K in Param]: string } : {};type Params = ParseRoute<'/users/:userId/posts/:postId'>;// { userId: string } & { postId: string }// Split a delimited literal into a tupletype Split<S extends string, D extends string> = S extends `${infer Head}${D}${infer Tail}` ? [Head, ...Split<Tail, D>] : [S];type Csv = Split<'a,b,c', ','>; // ['a', 'b', 'c']
Advanced Type-System Gotchas
Behaviors that surprise developers moving past beginner conditional/mapped types.
- Distributive vs. non-distributive- `T extends U ? X : Y` distributes over naked union type parameters; wrap T in `[T]` (e.g. `[T] extends [U]`) to compare the union as a whole instead
- any short-circuits conditionals- `T extends U ? X : Y` where T is `any` yields `X | Y`, not one branch - guard with `unknown` when you need precision
- Excess property checks vs. structural checks- Object literals get extra excess-property checks that assigned variables of the same shape don't - a common source of confusing 'works via variable, fails via literal' bugs
- Mapped type modifiers +/-- `{ -readonly [K in keyof T]-?: T[K] }` explicitly strips both readonly and optional modifiers rather than adding them
- keyof on index signatures- `keyof { [key: string]: unknown }` is `string | number`, not `string`, because numeric keys also match a string index signature
- Circular type alias limits- Deep recursive conditional types can hit TS's instantiation-depth limit ('Type instantiation is excessively deep') - flatten or cap recursion for very large unions
- Template literal type explosion- A template literal type over N-way unions produces a cross product of string literals - large unions can blow up compiler performance
Use the never type with a default switch case (const _exhaustive: never = value) to get a compile-time error whenever a new member is added to a discriminated union but a switch statement isn't updated to handle it.