TypeScript Generics Deep Dive Cheat Sheet
Covers generic functions, constraints with extends and keyof, generic classes with default type parameters, and common generic patterns.
Generic Functions
Write functions that preserve type information across their inputs and outputs.
function identity<T>(value: T): T { return value;}const num = identity<number>(42); // explicit type argumentconst str = identity('hi'); // inferred as stringfunction firstElement<T>(arr: T[]): T | undefined { return arr[0];}// Multiple type parametersfunction pair<A, B>(a: A, b: B): [A, B] { return [a, b];}
Generic Constraints
Restrict a type parameter to shapes that support the operations you need.
interface HasLength { length: number; }function longest<T extends HasLength>(a: T, b: T): T { return a.length >= b.length ? a : b; // T is guaranteed to have .length}longest('abc', 'ab'); // OKlongest([1, 2], [1]); // OK// longest(3, 5); // Error: number has no .length// keyof constraint - safe property accessfunction getProp<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key];}const user = { name: 'Ana', age: 30 };getProp(user, 'name'); // string
Generic Classes & Default Type Parameters
Parameterize an entire class and supply a fallback type.
class Box<T = unknown> { // default type parameter constructor(private value: T) {} get(): T { return this.value; } set(value: T): void { this.value = value; }}const numberBox = new Box<number>(42);const inferredBox = new Box('hello'); // Box<string>class Repository<T extends { id: number }> { private items: T[] = []; add(item: T) { this.items.push(item); } findById(id: number): T | undefined { return this.items.find(i => i.id === id); }}
Common Generic Patterns
Idioms that show up repeatedly in typed codebases.
- T extends U- Constrains T to types assignable to U
- K extends keyof T- Restricts K to be a valid property name of T
- Default type params- <T = DefaultType> supplies a fallback when no argument is given
- Generic interfaces- interface ApiResponse<T> { data: T; error?: string }
- Conditional generics- type NonNull<T> = T extends null | undefined ? never : T
- Generic constraints chaining- <T, K extends keyof T = keyof T> chains constraints between parameters
Building Generic Utility Types
Combine generics with async code for reusable, type-safe wrappers.
type ApiResponse<T> = { data: T; status: number; error?: string;};async function fetchJson<T>(url: string): Promise<ApiResponse<T>> { const res = await fetch(url); const data = await res.json(); return { data, status: res.status };}interface User { id: number; name: string; }const result = await fetchJson<User>('/api/user/1'); // result.data is User
Simulating Higher-Kinded Types
TypeScript has no native higher-kinded generics, but a type-map registry pattern approximates them.
interface HKT<URI, A> { readonly _URI: URI; readonly _A: A; }interface URItoKind<A> { array: A[]; option: A | undefined;}type URIS = keyof URItoKind<unknown>;type Kind<URI extends URIS, A> = URItoKind<A>[URI];interface Functor<F extends URIS> { map<A, B>(fa: Kind<F, A>, f: (a: A) => B): Kind<F, B>;}const arrayFunctor: Functor<'array'> = { map: (fa, f) => fa.map(f),};// Lets you write algorithms generic over 'container shape', not just element type
Variadic Tuple Types
Spread generic tuple parameters to model functions like compose, curry, and concat precisely.
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];type C = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]// Curry a function's first argument while preserving the resttype Curry<F> = F extends (first: infer A, ...rest: infer R) => infer Ret ? (first: A) => R extends [] ? Ret : (...rest: R) => Ret : never;function curry<F extends (...args: any[]) => any>(fn: F): Curry<F> { return ((first: any) => (...rest: any[]) => fn(first, ...rest)) as Curry<F>;}const add3 = (a: number, b: number, c: number) => a + b + c;const curried = curry(add3);curried(1)(2, 3); // 6, fully typed at each step
Function Overloads with Generic Narrowing
Overload signatures let a generic function return a more specific type per call shape than a single signature could express.
function query<T>(id: number): Promise<T>;function query<T>(ids: number[]): Promise<T[]>;function query<T>(idOrIds: number | number[]): Promise<T | T[]> { if (Array.isArray(idOrIds)) { return Promise.all(idOrIds.map((id) => fetchOne<T>(id))); } return fetchOne<T>(idOrIds);}async function fetchOne<T>(id: number): Promise<T> { return {} as T;}interface User { id: number; }const single = await query<User>(1); // Promise<User>const many = await query<User>([1, 2]); // Promise<User[]>
Fluent Builder with Progressive Generic Narrowing
Track 'which fields have been set' in the type parameter itself so .build() only compiles once everything required is present.
type RequiredKeys = 'name' | 'age';class PersonBuilder<Set extends string = never> { private data: Partial<{ name: string; age: number }> = {}; setName(name: string): PersonBuilder<Set | 'name'> { this.data.name = name; return this as any; } setAge(age: number): PersonBuilder<Set | 'age'> { this.data.age = age; return this as any; } // build() only exists on the type once all RequiredKeys are in Set build(this: PersonBuilder<RequiredKeys>): { name: string; age: number } { return this.data as { name: string; age: number }; }}const person = new PersonBuilder().setName('Ana').setAge(30).build(); // OK// new PersonBuilder().setName('Ana').build(); // Error: age not set
Advanced Generic Gotchas & Idioms
Subtleties that matter once you're writing library-grade generic code.
- Covariance vs. contravariance- Function parameter types are checked contravariantly (bivariantly for methods) - a function accepting a narrower type isn't assignable where a wider-accepting function is expected
- const type parameters (TS 5.0+)- `function f<const T>(x: T)` infers literal types instead of widening, avoiding the need for `as const` at every call site
- Generic defaults referencing earlier params- `<T, K extends keyof T = keyof T>` lets a later type parameter default based on an earlier one
- Inference from contextual typing- Passing a generic function as a callback lets TS infer its type parameters from the expected parameter types of the receiving function, not just its own arguments
- Avoid over-constraining with any- `<T extends any>` adds no constraint; prefer `<T extends unknown>` (equivalent, but clearer intent) or a real structural constraint
- Generic type inference caching- TS infers each type parameter once from the best common supertype of all inferred candidates - conflicting inferred positions can silently widen to `unknown` or a union
- Phantom type parameters- A type parameter that never appears in the value's runtime shape (only in a marker field) still narrows assignability, used for branding and state-machine builders
Let TypeScript infer generic type arguments from function arguments whenever possible instead of specifying them explicitly (identity(42) over identity<number>(42)) - explicit annotations should mainly be reserved for cases inference can't determine, like empty arrays.