TypeScript Generics: Complete Guide with Examples
Generics are TypeScript's most powerful feature and the topic most developers find difficult. This guide builds your understanding from first principles.
Why Generics Exist
Without generics, you must choose between safety and reusability:
// Too restrictive — only works for number[]
function first(arr: number[]): number { return arr[0] }
// Too permissive — loses type information function first(arr: any[]): any { return arr[0] }
// Just right — generic function first<T>(arr: T[]): T { return arr[0] } const n = first([1, 2, 3]) // n: number ✓ const s = first(['a', 'b']) // s: string ✓
The type parameter T is inferred from the argument — no manual annotation needed.
Generic Constraints (extends)
Without constraints, generic T is too wide — you can only use it as unknown.
// ❌ TypeScript can't assume T has .length
function logLength<T>(x: T) {
console.log(x.length) // Error: Property 'length' does not exist on type 'T'
}
// ✅ Constrain T to objects with a length property function logLength<T extends { length: number }>(x: T) { console.log(x.length) // works: string, array, typed arrays, NodeList... }
// Real example: type-safe property getter function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key] }
const user = { id: 1, name: 'Alice', email: 'a@b.com' } const name = getProperty(user, 'name') // string ✓ const id = getProperty(user, 'id') // number ✓ getProperty(user, 'missing') // ❌ Error: not a key of user
Generic Interfaces and Classes
interface Repository<T> {
findById(id: string): Promise<T | null>
findAll(): Promise<T[]>
save(entity: Omit<T, 'id'>): Promise<T>
delete(id: string): Promise<void>
}
class UserRepository implements Repository<User> { async findById(id: string) { / ... / } // TypeScript enforces all four methods match the interface }
// Generic class — type parameter used throughout 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[this.items.length - 1] } get size(): number { return this.items.length } }
const numStack = new Stack<number>() numStack.push(1) numStack.push('a') // ❌ Error: Argument of type 'string' is not assignable to 'number'
Multiple Type Parameters
function zip<A, B>(a: A[], b: B[]): [A, B][] {
return a.map((item, i) => [item, b[i]])
}
const pairs = zip([1,2,3], ['a','b','c']) // type: [number, string][]
// Generic with default type function useState<T = string>(initial: T): [T, (val: T) => void] { let value = initial return [value, (val) => { value = val }] } const [name, setName] = useState() // T defaults to string
The infer Keyword
infer introduces a type variable inside a conditional type to extract a type from another:
// Extract the return type of any function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type A = ReturnType<() => string> // string type B = ReturnType<(x: number) => boolean> // boolean
// Extract the type inside a Promise type Awaited<T> = T extends Promise<infer V> ? V : T type C = Awaited<Promise<User>> // User type D = Awaited<string> // string
// Extract element type from array type ArrayItem<T> = T extends (infer Item)[] ? Item : never type E = ArrayItem<User[]> // User
Mapped Types with Generics
// Make all properties optional
type Partial<T> = { [K in keyof T]?: T[K] }
// Make all properties required type Required<T> = { [K in keyof T]-?: T[K] }
// Pick a subset of keys type Pick<T, K extends keyof T> = { [P in K]: T[P] }
// Real-world example: form state derived from model type FormValues<T> = { [K in keyof T]: T[K] extends object ? string : T[K] }
Common Generic Patterns in Practice
// Type-safe event emitter
class TypedEventEmitter<Events extends Record<string, unknown>> {
on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void): void { / ... / }
emit<K extends keyof Events>(event: K, data: Events[K]): void { / ... / }
}
type AppEvents = { login: { userId: string } logout: { userId: string } error: Error }
const emitter = new TypedEventEmitter<AppEvents>() emitter.on('login', ({ userId }) => console.log(userId)) // userId: string ✓ emitter.emit('error', new Error('oops')) // correct type ✓ emitter.emit('login', 'wrong') // ❌ Type error
Practice TypeScript questions at [JSPrep Pro](/auth).