TypeScript Generics Cheat Sheet
Constraints, defaults, keyof patterns, conditional types, and common generic utilities -- all in one reference.
Basic Generic Function
A type parameter captures the type at the call site and carries it through the function signature.
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // => type: number
const str = identity('hello'); // => type: stringTypeScript infers T from the argument. You rarely need to pass it explicitly.
// In .tsx files, add a trailing comma to avoid JSX ambiguity
const toArray = <T,>(value: T): T[] => [value];
toArray(5); // => type: number[]
toArray('a'); // => type: string[]function parse<T>(raw: string): T {
return JSON.parse(raw) as T;
}
const user = parse<{ name: string; age: number }>('{"name":"Alice","age":30}');
// user.name => type: stringExplicit type arguments are needed when TypeScript can't infer T from the parameters.
Generic Interfaces and Types
Generics work on interfaces and type aliases the same way they work on functions.
interface ApiResponse<T> {
data: T;
status: number;
timestamp: Date;
}
const userResponse: ApiResponse<{ name: string }> = {
data: { name: 'Alice' },
status: 200,
timestamp: new Date(),
};type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
const success: Result<number> = { ok: true, value: 42 };
const failure: Result<number> = { ok: false, error: new Error('oops') };class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
peek(): T | undefined { return this.items.at(-1); }
}
const stack = new Stack<number>();
stack.push(10);
stack.peek(); // => type: number | undefinedGeneric Constraints (extends)
Use `extends` to restrict which types a generic parameter accepts.
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
logLength('hello'); // => OK: string has .length
logLength([1, 2, 3]); // => OK: array has .length
// logLength(42); // => Error: number has no .lengthfunction formatId<T extends string | number>(id: T): string {
return `ID-${id}`;
}
formatId(123); // => 'ID-123'
formatId('abc'); // => 'ID-abc'
// formatId(true); // => Error: boolean not assignable to string | numberkeyof + Generic Constraint Pattern
The most common generic pattern: safely access object properties with full type inference.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice', role: 'admin' as const };
getProperty(user, 'name'); // => type: string
getProperty(user, 'role'); // => type: 'admin'
// getProperty(user, 'foo'); // => Error: 'foo' not in 'id' | 'name' | 'role'function pluck<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map((key) => obj[key]);
}
const config = { host: 'localhost', port: 3000, debug: true };
pluck(config, ['host', 'port']); // => type: (string | number)[]Multiple Type Parameters
Use multiple type parameters when a function relates two or more types.
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
const lengths = mapArray(['hello', 'world'], (s) => s.length);
// lengths => type: number[]function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const p = pair('name', 42); // => type: [string, number]Generic Default Types
Defaults let callers omit type arguments when a sensible fallback exists.
interface PaginatedList<T, Meta = { page: number; total: number }> {
items: T[];
meta: Meta;
}
// Uses default Meta type
const users: PaginatedList<{ name: string }> = {
items: [{ name: 'Alice' }],
meta: { page: 1, total: 50 },
};
// Overrides Meta type
const tagged: PaginatedList<string, { cursor: string }> = {
items: ['a', 'b'],
meta: { cursor: 'abc123' },
};Default type parameters must come after required ones, just like default function parameters.
type EventMap = Record<string, unknown>;
class Emitter<Events extends EventMap = Record<string, never>> {
emit<K extends keyof Events>(event: K, data: Events[K]): void {
// implementation
}
}
interface AppEvents {
login: { userId: string };
logout: undefined;
}
const bus = new Emitter<AppEvents>();
bus.emit('login', { userId: '123' }); // => OK
// bus.emit('login', { wrong: true }); // => ErrorGeneric Utility Patterns
Common generic patterns you'll see (and write) in real TypeScript codebases.
function typedKeys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
const settings = { theme: 'dark', fontSize: 14 };
const keys = typedKeys(settings); // => type: ('theme' | 'fontSize')[]Object.keys returns string[] by design. This wrapper trades runtime safety for type convenience -- only use it when you control the object shape.
class QueryBuilder<T> {
private filters: Partial<T> = {};
where<K extends keyof T>(key: K, value: T[K]): this {
this.filters[key] = value;
return this;
}
build(): Partial<T> { return { ...this.filters }; }
}
interface User { name: string; age: number; active: boolean }
const query = new QueryBuilder<User>()
.where('age', 30)
.where('active', true)
.build();
// query => type: Partial<User>Conditional Types with Generics (infer)
Conditional types let you create types that depend on a condition, and `infer` extracts types from within other types.
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // => true
type B = IsString<42>; // => false
type C = IsString<string>; // => true// Extract the return type of a function
type GetReturn<T> = T extends (...args: unknown[]) => infer R ? R : never;
type A = GetReturn<() => string>; // => string
type B = GetReturn<(x: number) => boolean>; // => boolean
// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type C = ElementOf<string[]>; // => string
type D = ElementOf<number[]>; // => numberThe built-in ReturnType<T> and Parameters<T> use this exact infer pattern under the hood.
type ToArray<T> = T extends unknown ? T[] : never;
// Distributes over union members
type A = ToArray<string | number>; // => string[] | number[]
// Prevent distribution with []
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type B = ToArrayNonDist<string | number>; // => (string | number)[]Generic Gotchas
Common mistakes and surprising behavior with TypeScript generics.
function isArrayOf<T>(arr: unknown[]): arr is T[] {
// T doesn't exist at runtime -- you can't check it here
// This compiles but does nothing useful:
return Array.isArray(arr); // Always true for any array
}
// Instead, pass a type guard as a parameter:
function isArrayOfType<T>(
arr: unknown[],
guard: (item: unknown) => item is T
): arr is T[] {
return arr.every(guard);
}Generic type parameters are erased at compile time. You can't use T in runtime checks like typeof or instanceof.
// This looks correct but fails:
function merge<T extends object>(a: T, b: T): T {
return { ...a, ...b };
// Error: spread type can only be created from object types
// The spread produces { ...T, ...T } which TS can't verify is T
}
// Fix: widen the return type
function merge<T extends object>(a: T, b: Partial<T>): T {
return { ...a, ...b } as T;
}// Overloads don't infer generics from implementation
function wrap<T>(value: T): { wrapped: T };
function wrap(value: unknown): { wrapped: unknown } {
return { wrapped: value };
}
const result = wrap(42); // => type: { wrapped: number }TypeScript picks the first overload signature whose parameters match. Put more specific overloads first.
Can you write this from memory?
Write a generic identity function that takes type T and returns T