TS Generics
Targeting TypeScript 5.x
Syntax
Generics
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
first<number>([1, 2, 3]) // 1
first(['a', 'b']) // 'a' (inferred)Generic Interfaces
interface Box<T> {
value: T
}
const strBox: Box<string> = { value: 'hello' }
const numBox: Box<number> = { value: 42 }
interface Pair<K, V> {
key: K
value: V
}
const entry: Pair<string, number> = {
key: 'age',
value: 28,
}Generic Default Types
interface ApiResponse<T = unknown> {
data: T
status: number
}
// uses default — data is unknown
const res: ApiResponse = { data: null, status: 200 }
// override — data is string
const named: ApiResponse<string> = {
data: 'hello',
status: 200,
}Generic Constraints
// T must have a length property
function longest<T extends { length: number }>(
a: T, b: T
): T {
return a.length >= b.length ? a : b
}
longest('abc', 'de') // 'abc'
longest([1, 2], [1]) // [1, 2]keyof + Generics
function getProperty<T, K extends keyof T>(
obj: T, key: K
): T[K] {
return obj[key]
}
const user = { name: 'Tammy', age: 28 }
getProperty(user, 'name') // string
getProperty(user, 'age') // number
getProperty(user, 'foo') // ErrorBeginner Patterns
Mapped Types
type Optional<T> = {
[K in keyof T]?: T[K]
}
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type UserGetters = Getters<{ name: string; age: number }>
// { getName: () => string; getAge: () => number }Conditional Types
// T extends U ? X : Y
type IsString<T> = T extends string ? true : false
type A = IsString<'hello'> // true
type B = IsString<42> // false
// practical: extract return type
type ReturnOf<T> = T extends (...args: any[]) => infer R
? R
: never
type N = ReturnOf<() => number> // numberInfer Keyword
// extract types from within other types
type UnpackPromise<T> =
T extends Promise<infer U> ? U : T
type A = UnpackPromise<Promise<string>> // string
type B = UnpackPromise<number> // number
// extract array element type
type ElementOf<T> =
T extends (infer E)[] ? E : never
type C = ElementOf<string[]> // stringIntermediate Patterns
Template Literal Types
type Color = 'red' | 'blue'
type Size = 'sm' | 'lg'
type ClassName = `${Size}-${Color}`
// 'sm-red' | 'sm-blue' | 'lg-red' | 'lg-blue'
type EventName<T extends string> = `on${Capitalize<T>}`
type ClickEvent = EventName<'click'> // 'onClick'Distributive Conditionals
// unions distribute over conditional types
type ToArray<T> = T extends any ? T[] : never
type A = ToArray<string | number>
// string[] | number[] (not (string | number)[])
// prevent distribution with tuple wrapping
type ToArrayND<T> = [T] extends [any] ? T[] : never
type B = ToArrayND<string | number>
// (string | number)[]Recursive Types
// JSON value — references itself
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json }
// deeply nested path type
type NestedKeys<T> = T extends object
? { [K in keyof T]: K | `${K & string}.${NestedKeys<T[K]> & string}` }[keyof T]
: neverAdvanced Patterns
Generic Classes
class Stack<T> {
private items: T[] = []
push(item: T) { this.items.push(item) }
pop(): T | undefined { return this.items.pop() }
peek(): T | undefined { return this.items.at(-1) }
}
const nums = new Stack<number>()
nums.push(1)
nums.push(2)
nums.pop() // 2Generic Utility Functions
// typed Object.keys (unsafe cast — TS omits this on purpose)
function typedKeys<T extends object>(obj: T) {
return Object.keys(obj) as (keyof T)[]
}
// type-safe groupBy
function groupBy<T, K extends string>(
arr: T[],
fn: (item: T) => K
): Record<K, T[]> {
return arr.reduce((acc, item) => {
const key = fn(item)
;(acc[key] ??= []).push(item)
return acc
}, {} as Record<K, T[]>)
}Last updated on