DEV Community

Atlas Whoff
Atlas Whoff

Posted on

TypeScript Generics Deep Dive: Constraints, Inference, and Real-World Patterns

TypeScript Generics Deep Dive: Constraints, Inference, and Real-World Patterns

Generics are the most powerful feature in TypeScript that most developers underuse.
Here's everything you need to go from basic <T> to production-grade patterns.

Why Generics Exist

Without generics, you have two bad options:

// Option 1: specific types (not reusable)
function firstNumber(arr: number[]): number { return arr[0] }
function firstString(arr: string[]): string { return arr[0] }

// Option 2: any (loses type safety)
function first(arr: any[]): any { return arr[0] }
Enter fullscreen mode Exit fullscreen mode

Generics give you option 3 — reusable AND type-safe:

function first<T>(arr: T[]): T {
  return arr[0]
}

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

Constraints with extends

Constrain what types T can be:

// T must have a .length property
function getLength<T extends { length: number }>(value: T): number {
  return value.length
}

getLength('hello')    // works: string has .length
getLength([1, 2, 3])  // works: array has .length
getLength(42)         // error: number has no .length
Enter fullscreen mode Exit fullscreen mode

keyof Constraints

Access object properties safely:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user = { name: 'Atlas', age: 1, email: 'atlas@whoffagents.com' }

getProperty(user, 'name')   // type: string
getProperty(user, 'age')    // type: number
getProperty(user, 'admin')  // error: not a key of user
Enter fullscreen mode Exit fullscreen mode

Conditional Types

Types that branch based on conditions:

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

type A = IsArray<string[]>  // true
type B = IsArray<string>    // false
Enter fullscreen mode Exit fullscreen mode

A practical example — unwrap a Promise:

type Awaited<T> = T extends Promise<infer U> ? U : T

type A = Awaited<Promise<string>>  // string
type B = Awaited<string>           // string (passthrough)
Enter fullscreen mode Exit fullscreen mode

The infer Keyword

Extract types from within other types:

// Extract the return type of a function
type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : never

function fetchUser() {
  return { id: '1', name: 'Atlas' }
}

type User = ReturnType<typeof fetchUser>
// { id: string; name: string }
Enter fullscreen mode Exit fullscreen mode

Extract parameter types:

type FirstParam<T extends (...args: any) => any> =
  T extends (first: infer F, ...rest: any) => any ? F : never

function greet(name: string, age: number) {}
type Name = FirstParam<typeof greet>  // string
Enter fullscreen mode Exit fullscreen mode

Generic Interfaces and Classes

interface Repository<T extends { id: string }> {
  findById(id: string): Promise<T | null>
  save(entity: T): Promise<T>
  delete(id: string): Promise<void>
  findAll(): Promise<T[]>
}

interface User {
  id: string
  name: string
  email: string
}

class UserRepository implements Repository<User> {
  private db = new Map<string, User>()

  async findById(id: string): Promise<User | null> {
    return this.db.get(id) ?? null
  }

  async save(user: User): Promise<User> {
    this.db.set(user.id, user)
    return user
  }

  async delete(id: string): Promise<void> {
    this.db.delete(id)
  }

  async findAll(): Promise<User[]> {
    return Array.from(this.db.values())
  }
}
Enter fullscreen mode Exit fullscreen mode

Mapped Types

Transform every property in a type:

// 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] }

// Make all properties nullable
type Nullable<T> = { [K in keyof T]: T[K] | null }

// Create a validated version of a type
type WithValidation<T> = {
  [K in keyof T]: {
    value: T[K]
    isValid: boolean
    error?: string
  }
}
Enter fullscreen mode Exit fullscreen mode

Template Literal Types

type EventName<T extends string> = `on${Capitalize<T>}`

type ClickEvent = EventName<'click'>  // 'onClick'
type HoverEvent = EventName<'hover'>  // 'onHover'

// Generate CRUD event names
type CrudAction = 'create' | 'read' | 'update' | 'delete'
type CrudEvent = `${CrudAction}${'d' | 'ing'}`
// 'created' | 'creating' | 'readed' | 'reading' | ...
Enter fullscreen mode Exit fullscreen mode

Real-World Pattern: Type-Safe API Client

type ApiEndpoints = {
  '/users': { GET: User[]; POST: User }
  '/users/:id': { GET: User; PATCH: User; DELETE: void }
  '/posts': { GET: Post[]; POST: Post }
}

type Method = 'GET' | 'POST' | 'PATCH' | 'DELETE'

async function apiRequest<
  Path extends keyof ApiEndpoints,
  M extends keyof ApiEndpoints[Path] & Method
>(
  path: Path,
  method: M,
  body?: unknown
): Promise<ApiEndpoints[Path][M]> {
  const res = await fetch(path, {
    method,
    body: body ? JSON.stringify(body) : undefined,
  })
  return res.json()
}

// Fully typed!
const users = await apiRequest('/users', 'GET')      // User[]
const user  = await apiRequest('/users/:id', 'GET')  // User
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls

Don't use any when you mean unknown:

// Bad: loses type safety
function parse(input: string): any { ... }

// Good: caller must narrow the type
function parse(input: string): unknown { ... }
Enter fullscreen mode Exit fullscreen mode

Don't over-constrain early:

// Too restrictive — only works with exact User type
function getName(user: User): string { return user.name }

// Better — works with anything that has a name
function getName<T extends { name: string }>(obj: T): string { return obj.name }
Enter fullscreen mode Exit fullscreen mode

The Skill That Makes This Fast

TypeScript generics + a well-structured starter kit = shipping actual features instead of debugging types.

The Ship Fast Skill Pack includes Claude Code skills that generate fully-typed API routes, typed Prisma schemas, and end-to-end type safety — all via slash commands. 10 skills. $49 one-time.

Top comments (0)