Skip to Content
Grab N GoTS Generics

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') // Error

Beginner 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> // number
Infer 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[]> // string

Intermediate 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] : never

Advanced 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() // 2
Generic 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