Skip to main content
Web DevelopmentTutorials

TypeScript Generics Demystified: From Basics to Advanced Patterns

Finally understand TypeScript generics. From simple type parameters to conditional types and mapped types with real-world examples.

Mbeah Essilfie

Mbeah Essilfie

June 5, 2026 at 06:03 AM

12 min read 912 views
TypeScript Generics Demystified: From Basics to Advanced Patterns

Why Generics?

Generics let you write code that works with any type while still maintaining type safety. Without them, you'd either lose type information or duplicate code for every type.

The Basics

// Without generics — loses type info
function first(arr: any[]): any {
  return arr[0]
}

// With generics — preserves type
function first<T>(arr: T[]): T | undefined {
  return arr[0]
}

const num = first([1, 2, 3])      // type: number
const str = first(['a', 'b'])      // type: string

Constraints

Limit what types are accepted:

interface HasId {
  id: string
}

function findById<T extends HasId>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id)
}

// Works with any object that has an id
findById(users, '123')  // returns User | undefined
findById(posts, '456')  // returns Post | undefined

Multiple Type Parameters

function merge<A, B>(objA: A, objB: B): A & B {
  return { ...objA, ...objB }
}

const result = merge({ name: 'Mbeah' }, { role: 'developer' })
// type: { name: string } & { role: string }

Real-World: Type-Safe Event Emitter

type EventMap = {
  'user:login': { userId: string; timestamp: number }
  'user:logout': { userId: string }
  'post:created': { postId: string; title: string }
}

class TypedEmitter<Events extends Record<string, any>> {
  private handlers = new Map<string, Function[]>()

  on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void) {
    const list = this.handlers.get(event as string) || []
    list.push(handler)
    this.handlers.set(event as string, list)
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]) {
    const list = this.handlers.get(event as string) || []
    list.forEach(fn => fn(payload))
  }
}

const emitter = new TypedEmitter<EventMap>()

emitter.on('user:login', (payload) => {
  // payload is typed as { userId: string; timestamp: number }
  console.log(payload.userId)
})

// emitter.emit('user:login', { userId: '123' })
// ❌ Error: missing 'timestamp'
Generic constraints with keyof ensure that event names and their payloads always match. Impossible to emit the wrong data shape.

Conditional Types

type IsArray<T> = T extends any[] ? true : false

type A = IsArray<string[]>  // true
type B = IsArray<number>    // false

// Practical: extract promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T

type X = Unwrap<Promise<string>>  // string
type Y = Unwrap<number>           // number

Mapped Types

// Make all properties optional
type Partial<T> = {
  [K in keyof T]?: T[K]
}

// Make all properties readonly
type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}

// Pick specific properties
type ApiResponse<T> = {
  [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
}

type UserApi = ApiResponse<{ name: string; age: number }>
// { getName: () => string; getAge: () => number }

Key Insight

Think of generics as functions for types. Just as regular functions take values and return values, generic types take types and return types.

Start simple, add constraints when needed, and let TypeScript's inference do the heavy lifting.

JavaScriptTypeScript
Mbeah Essilfie

Written by Mbeah Essilfie

Fullstack Software Developer

Read next

The Complete Guide to Vue 3 Composables
12 min read

The Complete Guide to Vue 3 Composables

Understand the power of Vue 3's Composition API through practical, real-world composable patterns that you can use today.

Mbeah EssilfieMbeah Essilfie
990