Deep Dive10 min read · Updated 2025-06-01

TypeScript Generics: Complete Guide with Examples

Master TypeScript generics from basic type parameters to constraints, conditional types, and infer. Includes real-world patterns used in libraries and production code.

💡 Practice these concepts interactively with AI feedback

Start Practicing →

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).

Put This Into Practice

Reading articles is passive. JSPrep Pro makes you actively recall, predict output, and get AI feedback.

Start Free →Browse All Questions

Related Articles

Deep Dive
We Built a RAG-Powered AI Question Engine Into a JavaScript Interview Platform — Here's Exactly How It Works
12 min read
Build Systems
Monorepo with Turborepo vs Nx: The Complete Comparison (2025)
9 min read
Core Concepts
map() vs forEach() in JavaScript: Which One to Use and Why It Matters
7 min read