Overview
Generics allow you to write code that works with different types while maintaining full type safety. Master this advanced feature for powerful abstractions.
Concepts
- Type Variables: Capture types as parameters
- Constraints: Limit what types can be passed
- Inference: TypeScript automatically figures out types
- Default Types: Provide fallback types
- Variance: Understand type relationships
Basic Generics
// Generic function
function identity<T>(arg: T): T {
return arg
}
const num = identity<number>(42)
const str = identity<string>('hello')
// Inferred types
const str2 = identity('world') // T inferred as string
Generic Constraints
interface HasLength {
length: number
}
// Type must have 'length' property
function getLength<T extends HasLength>(arg: T): number {
return arg.length
}
getLength('hello') // ✓ strings have length
getLength([1, 2, 3]) // ✓ arrays have length
getLength(123) // ✗ numbers don't have length
Generic with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
interface User {
id: number
name: string
}
const user: User = { id: 1, name: 'Alice' }
const id = getProperty(user, 'id') // Type: number
const name = getProperty(user, 'name') // Type: string
getProperty(user, 'email') // ✗ Type error: 'email' not in User
Generic Classes
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]
}
}
const numberStack = new Stack<number>()
numberStack.push(1)
numberStack.push(2)
const value = numberStack.pop() // Type: number | undefined
Conditional Generic Types
type IsString<T> = T extends string ? true : false
type A = IsString<'hello'> // true
type B = IsString<number> // false
// Practical example
type Flatten<T> = T extends Array<infer U> ? U : T
type Str = Flatten<string[]> // string
type Num = Flatten<number> // number
Generics reduce code duplication by 50% while maintaining type safety.