TypeScript — Generics
Generics allow you to write functions, interfaces, and classes that work with any type while preserving type safety. Instead of using any, generics let you capture the relationship between input and output types — the type flows through your code.
The identity function is the classic generic example: it takes a value and returns it unchanged, preserving the input type.
| 1 | // ❌ Without generics — loses type information |
| 2 | function identity(arg: any): any { |
| 3 | return arg; |
| 4 | } |
| 5 | const result = identity("hello"); // type: any (no safety) |
| 6 | |
| 7 | // ✅ With generics — type is captured and preserved |
| 8 | function identityGeneric<T>(arg: T): T { |
| 9 | return arg; |
| 10 | } |
| 11 | const str = identityGeneric("hello"); // type: string |
| 12 | const num = identityGeneric(42); // type: number |
| 13 | const arr = identityGeneric([1, 2]); // type: number[] |
| 14 | |
| 15 | // Multiple type parameters |
| 16 | function pair<A, B>(first: A, second: B): [A, B] { |
| 17 | return [first, second]; |
| 18 | } |
| 19 | const p = pair("hello", 42); // [string, number] |
| 20 | |
| 21 | // Generic arrow function |
| 22 | const wrap = <T>(value: T): { value: T } => ({ value }); |
| 23 | |
| 24 | // Generic with conditional return |
| 25 | function first<T>(arr: T[]): T | undefined { |
| 26 | return arr[0]; |
| 27 | } |
| 28 | |
| 29 | const firstNum = first([1, 2, 3]); // number | undefined |
| 30 | const firstStr = first(["a", "b"]); // string | undefined |
| 31 | const firstEmpty = first([]); // undefined |
TypeScript usually infers type parameters from the arguments. You only need explicit type arguments when inference isn't possible or when you want a specific type.
| 1 | // Inferred from arguments |
| 2 | function wrap<T>(value: T): { value: T; timestamp: number } { |
| 3 | return { value, timestamp: Date.now() }; |
| 4 | } |
| 5 | const w = wrap("hello"); // inferred T = string |
| 6 | |
| 7 | // Explicit type argument — needed when inference is ambiguous |
| 8 | function create<T>(value: T): T[] { |
| 9 | return [value]; |
| 10 | } |
| 11 | const nums = create<number>(42); // number[] — explicit |
| 12 | const mixed = create(42); // number[] — inferred |
| 13 | |
| 14 | // Inference from multiple positions |
| 15 | function map<T, U>(arr: T[], fn: (item: T) => U): U[] { |
| 16 | return arr.map(fn); |
| 17 | } |
| 18 | // T inferred from arr, U inferred from fn return type |
| 19 | const lengths = map(["hello", "world"], (s) => s.length); |
| 20 | // T = string, U = number → number[] |
| 21 | |
| 22 | // Inference context — better inference with callbacks |
| 23 | function reduce<T, U>( |
| 24 | arr: T[], |
| 25 | fn: (acc: U, item: T) => U, |
| 26 | initial: U |
| 27 | ): U { |
| 28 | return arr.reduce(fn, initial); |
| 29 | } |
| 30 | // U inferred from initial value type |
| 31 | const total = reduce( |
| 32 | [1, 2, 3], |
| 33 | (acc, n) => acc + n, |
| 34 | 0 // initial: 0 → U = number |
| 35 | ); |
| 36 | |
| 37 | // When inference fails — provide the type argument |
| 38 | function merge<T extends object>(a: T, b: Partial<T>): T { |
| 39 | return { ...a, ...b }; |
| 40 | } |
| 41 | const merged = merge({ x: 1, y: 2 }, { y: 10 }); // inferred |
| 42 | // If inference fails: merge<{ x: number; y: number }>(a, b) |
info
Constraints restrict which types a generic can accept. Use extends to require that a type parameter satisfies a particular interface or shape.
| 1 | // Constraint: T must have a .length property |
| 2 | function logLength<T extends { length: number }>(item: T): T { |
| 3 | console.log(item.length); |
| 4 | return item; |
| 5 | } |
| 6 | |
| 7 | logLength("hello"); // OK — string has .length |
| 8 | logLength([1, 2, 3]); // OK — array has .length |
| 9 | // logLength(42); // Error: number doesn't have .length |
| 10 | |
| 11 | // Constraint with keyof |
| 12 | function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { |
| 13 | return obj[key]; |
| 14 | } |
| 15 | |
| 16 | const person = { name: "Alice", age: 30 }; |
| 17 | getProperty(person, "name"); // string |
| 18 | getProperty(person, "age"); // number |
| 19 | // getProperty(person, "email"); // Error: "email" not in keyof person |
| 20 | |
| 21 | // Chained constraints |
| 22 | interface HasId { |
| 23 | id: number; |
| 24 | } |
| 25 | |
| 26 | interface HasTimestamp { |
| 27 | createdAt: Date; |
| 28 | } |
| 29 | |
| 30 | function findById<T extends HasId & HasTimestamp>( |
| 31 | items: T[], |
| 32 | id: number |
| 33 | ): T | undefined { |
| 34 | return items.find((item) => item.id === id); |
| 35 | } |
| 36 | |
| 37 | // Real-world: validating an object has required keys |
| 38 | function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { |
| 39 | const result = {} as Pick<T, K>; |
| 40 | for (const key of keys) { |
| 41 | result[key] = obj[key]; |
| 42 | } |
| 43 | return result; |
| 44 | } |
| 45 | |
| 46 | const user = { name: "Alice", age: 30, email: "a@b.com" }; |
| 47 | const picked = pick(user, ["name", "email"]); |
| 48 | // { name: string; email: string } |
| 1 | // Generic interface — reusable container |
| 2 | interface ApiResponse<T> { |
| 3 | data: T; |
| 4 | status: number; |
| 5 | timestamp: Date; |
| 6 | error?: string; |
| 7 | } |
| 8 | |
| 9 | const userResponse: ApiResponse<{ name: string; age: number }> = { |
| 10 | data: { name: "Alice", age: 30 }, |
| 11 | status: 200, |
| 12 | timestamp: new Date(), |
| 13 | }; |
| 14 | |
| 15 | const listResponse: ApiResponse<string[]> = { |
| 16 | data: ["a", "b", "c"], |
| 17 | status: 200, |
| 18 | timestamp: new Date(), |
| 19 | }; |
| 20 | |
| 21 | // Generic interface as a map/dictionary |
| 22 | interface Cache<T> { |
| 23 | get(key: string): T | undefined; |
| 24 | set(key: string, value: T): void; |
| 25 | has(key: string): boolean; |
| 26 | delete(key: string): void; |
| 27 | } |
| 28 | |
| 29 | // Generic interface with extends |
| 30 | interface Paginated<T> { |
| 31 | items: T[]; |
| 32 | total: number; |
| 33 | page: number; |
| 34 | pageSize: number; |
| 35 | hasNext(): boolean; |
| 36 | } |
| 37 | |
| 38 | // Generic type predicate interface |
| 39 | interface Predicate<T> { |
| 40 | (item: T): boolean; |
| 41 | } |
| 42 | |
| 43 | const isPositive: Predicate<number> = (n) => n > 0; |
| 44 | const isNonEmpty: Predicate<string> = (s) => s.length > 0; |
| 45 | |
| 46 | // Generic event emitter |
| 47 | interface EventEmitter<Events extends Record<string, unknown[]>> { |
| 48 | on<K extends keyof Events>( |
| 49 | event: K, |
| 50 | handler: (...args: Events[K]) => void |
| 51 | ): void; |
| 52 | emit<K extends keyof Events>(event: K, ...args: Events[K]): void; |
| 53 | } |
| 54 | |
| 55 | type AppEvents = { |
| 56 | login: [username: string]; |
| 57 | logout: []; |
| 58 | error: [code: number, message: string]; |
| 59 | }; |
| 60 | |
| 61 | declare const emitter: EventEmitter<AppEvents>; |
| 62 | emitter.on("login", (username) => console.log(username)); // string |
| 63 | emitter.on("error", (code, msg) => {}); // number, string |
| 1 | // Generic class — type-safe container |
| 2 | class Stack<T> { |
| 3 | private items: T[] = []; |
| 4 | |
| 5 | push(item: T): void { |
| 6 | this.items.push(item); |
| 7 | } |
| 8 | |
| 9 | pop(): T | undefined { |
| 10 | return this.items.pop(); |
| 11 | } |
| 12 | |
| 13 | peek(): T | undefined { |
| 14 | return this.items[this.items.length - 1]; |
| 15 | } |
| 16 | |
| 17 | get size(): number { |
| 18 | return this.items.length; |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | const numStack = new Stack<number>(); |
| 23 | numStack.push(1); |
| 24 | numStack.push(2); |
| 25 | const top = numStack.pop(); // number | undefined |
| 26 | |
| 27 | // Generic class with constraint |
| 28 | class SortedList<T extends { compareTo(other: T): number }> { |
| 29 | private items: T[] = []; |
| 30 | |
| 31 | insert(item: T): void { |
| 32 | const idx = this.items.findIndex( |
| 33 | (existing) => item.compareTo(existing) < 0 |
| 34 | ); |
| 35 | this.items.splice(idx === -1 ? this.items.length : idx, 0, item); |
| 36 | } |
| 37 | |
| 38 | getAll(): readonly T[] { |
| 39 | return this.items; |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | interface Comparable { |
| 44 | compareTo(other: Comparable): number; |
| 45 | } |
| 46 | |
| 47 | // Generic class implementing an interface |
| 48 | class EventEmitter<T extends Record<string, unknown[]>> { |
| 49 | private handlers: { |
| 50 | [K in keyof T]?: Array<(...args: T[K]) => void>; |
| 51 | } = {}; |
| 52 | |
| 53 | on<K extends keyof T>(event: K, handler: (...args: T[K]) => void): void { |
| 54 | if (!this.handlers[event]) this.handlers[event] = []; |
| 55 | this.handlers[event]!.push(handler); |
| 56 | } |
| 57 | |
| 58 | emit<K extends keyof T>(event: K, ...args: T[K]): void { |
| 59 | this.handlers[event]?.forEach((handler) => handler(...args)); |
| 60 | } |
| 61 | } |
Default type parameters let callers omit the type argument when the default is sufficient. They are commonly used in library APIs and React component types.
| 1 | // Default type parameter |
| 2 | interface ApiResponse<T = unknown> { |
| 3 | data: T; |
| 4 | status: number; |
| 5 | } |
| 6 | |
| 7 | // Caller can omit T — defaults to unknown |
| 8 | const response: ApiResponse = { data: "hello", status: 200 }; |
| 9 | // data is unknown — you must narrow before using |
| 10 | |
| 11 | // Caller can provide T for type safety |
| 12 | const typedResponse: ApiResponse<{ name: string }> = { |
| 13 | data: { name: "Alice" }, |
| 14 | status: 200, |
| 15 | }; |
| 16 | |
| 17 | // Multiple defaults |
| 18 | interface Store< |
| 19 | State = Record<string, unknown>, |
| 20 | Actions extends Record<string, (...args: any[]) => any> = Record<string, never> |
| 21 | > { |
| 22 | state: State; |
| 23 | dispatch: <K extends keyof Actions>( |
| 24 | action: K, |
| 25 | ...args: Parameters<Actions[K]> |
| 26 | ): ReturnType<Actions[K]>; |
| 27 | } |
| 28 | |
| 29 | // Partial defaults — only some parameters specified |
| 30 | type MyStore = Store<{ count: number }>; |
| 31 | |
| 32 | // Real-world: React-style props pattern |
| 33 | interface ComponentProps<T = Record<string, never>> { |
| 34 | children?: React.ReactNode; |
| 35 | className?: string; |
| 36 | data?: T; |
| 37 | } |
| 38 | |
| 39 | // Simple component — no data prop needed |
| 40 | const Card: React.FC<ComponentProps> = ({ children, className }) => ( |
| 41 | <div className={className}>{children}</div> |
| 42 | ); |
| 43 | |
| 44 | // Data component — data is typed |
| 45 | interface User { |
| 46 | name: string; |
| 47 | email: string; |
| 48 | } |
| 49 | |
| 50 | const UserCard: React.FC<ComponentProps<User>> = ({ data }) => ( |
| 51 | <div>{data?.name}</div> |
| 52 | ); |
info
| 1 | // TypeScript provides built-in utility types that use generics |
| 2 | |
| 3 | // Partial<T> — all properties optional |
| 4 | function updateUser<T extends object>(base: T, updates: Partial<T>): T { |
| 5 | return { ...base, ...updates }; |
| 6 | } |
| 7 | |
| 8 | // Required<T> — all properties required |
| 9 | type StrictConfig = Required<{ host?: string; port?: number }>; |
| 10 | |
| 11 | // Pick<T, K> — select specific properties |
| 12 | function pickUser<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { |
| 13 | return keys.reduce( |
| 14 | (acc, key) => ({ ...acc, [key]: obj[key] }), |
| 15 | {} as Pick<T, K> |
| 16 | ); |
| 17 | } |
| 18 | |
| 19 | // Omit<T, K> — exclude properties |
| 20 | type PublicUser = Omit<User, "password" | "salt">; |
| 21 | |
| 22 | // Record<K, V> — map with typed keys |
| 23 | function groupBy<T, K extends string>( |
| 24 | items: T[], |
| 25 | keyFn: (item: T) => K |
| 26 | ): Record<K, T[]> { |
| 27 | return items.reduce((acc, item) => { |
| 28 | const key = keyFn(item); |
| 29 | if (!acc[key]) acc[key] = []; |
| 30 | acc[key].push(item); |
| 31 | return acc; |
| 32 | }, {} as Record<K, T[]>); |
| 33 | } |
| 34 | |
| 35 | // ReturnType<T> — extract function return type |
| 36 | function createDefault() { |
| 37 | return { name: "", active: true }; |
| 38 | } |
| 39 | type Default = ReturnType<typeof createDefault>; |
| 40 | |
| 41 | // Parameters<T> — extract function parameter types |
| 42 | function fetchUser(id: number, includePosts: boolean) { |
| 43 | return { id, includePosts }; |
| 44 | } |
| 45 | type FetchArgs = Parameters<typeof fetchUser>; // [number, boolean] |
| 46 | |
| 47 | // Extract<T, U> and Exclude<T, U> |
| 48 | type Numbers = Extract<string | number | boolean, number>; // number |
| 49 | type NotBool = Exclude<string | number | boolean, boolean>; // string | number |
1. Let TypeScript infer type parameters whenever possible. Only provide explicit type arguments when inference fails or you need a specific narrowing.
2. Use meaningful names for type parameters: T for types, K for keys, V for values, E for errors.
3. Constrain generics with extends when you need specific properties — it improves autocompletion and catches errors early.
4. Avoid any in generic positions — it defeats the purpose. Use unknown if you truly need flexibility.
5. Use default type parameters for library APIs — they make simple cases easy while preserving advanced use cases.
6. Compose built-in utility types (Partial, Pick, Omit) instead of rewriting them — they are well-tested and readable.
7. Generic constraints with keyof are powerful for type-safe property access patterns like getProperty<T, K extends keyof T>.
8. Prefer generic functions over any-typed functions — generics preserve the relationship between input and output types.