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] }
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
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
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
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
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)
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 }
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
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())
}
}
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
}
}
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' | ...
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
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 { ... }
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 }
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)