# TypeScript Generics Patterns Reference ## Table of Contents 1. [Generic Functions](#generic-functions) 2. [Generic Classes](#generic-classes) 3. [Generic Interfaces](#generic-interfaces) 4. [Conditional Types](#conditional-types) 5. [Mapped Types](#mapped-types) 6. [Template Literal Types in Generics](#template-literal-types-in-generics) 7. [Variadic Tuple Types](#variadic-tuple-types) 8. [Higher-Kinded Types](#higher-kinded-types) 9. [Builder Pattern](#builder-pattern) 10. [Common Generic Patterns](#common-generic-patterns) --- ## Generic Functions ### Infer Type Parameters From Arguments TypeScript infers type parameters from call-site arguments. Prefer inference over explicit type args. ```typescript // Inferred: T = string from the argument function identity(value: T): T { return value; } const s = identity('hello'); // T inferred as string // Constraint: T must have a length property function longest(a: T, b: T): T { return a.length >= b.length ? a : b; } longest('alice', 'bob'); // OK - strings have length longest([1, 2, 3], [1, 2]); // OK - arrays have length longest({ length: 5 }, { length: 3 }); // OK // Multiple type parameters with relationship constraint function getProperty(obj: T, key: K): T[K] { return obj[key]; } const user = { id: 1, name: 'Alice', active: true }; getProperty(user, 'name'); // string getProperty(user, 'active'); // boolean getProperty(user, 'foo'); // Error: 'foo' is not a key of typeof user ``` ### Use Default Type Parameters ```typescript // Default type parameter when not specified function createArray(length: number, fill: T): T[] { return Array(length).fill(fill); } const strings = createArray(3, 'x'); // string[] - T inferred const numbers = createArray(3, 0); // number[] - T inferred const explicit = createArray(3, true); // boolean[] // Useful in generic components/hooks interface PaginatedResponse { data: T[]; total: number; page: number; } ``` --- ## Generic Classes ### Parameterize Class Behavior ```typescript class Stack { private items: T[] = []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } peek(): T | undefined { return this.items[this.items.length - 1]; } get size(): number { return this.items.length; } } const numStack = new Stack(); numStack.push(1); numStack.push(2); const top = numStack.pop(); // number | undefined ``` ### Recognize the Static Members Limitation Static members cannot reference a class's type parameters. The type parameter belongs to an instance. ```typescript class Container { value: T; // OK - instance member constructor(value: T) { this.value = value; } // static defaultValue: T; // Error: static members can't reference type parameters // Workaround: use a separate factory type or factory method static create(value: U): Container { return new Container(value); } } ``` --- ## Generic Interfaces ### Implement the Repository Pattern ```typescript interface Repository { findById(id: ID): Promise; findAll(filter?: Partial): Promise; save(entity: T): Promise; delete(id: ID): Promise; } interface User { id: string; name: string; email: string; } class UserRepository implements Repository { async findById(id: string): Promise { /* ... */ return null; } async findAll(filter?: Partial): Promise { /* ... */ return []; } async save(entity: User): Promise { /* ... */ return entity; } async delete(id: string): Promise { /* ... */ } } ``` ### Implement the Factory Pattern ```typescript interface Factory { create(...args: TArgs): T; } class ConnectionFactory implements Factory { create(host: string, port: number): Connection { return new Connection(host, port); } } ``` --- ## Conditional Types ### Distribute Over Union Types Conditional types distribute over naked type parameters in unions. ```typescript // Distributes: IsString = IsString | IsString type IsString = T extends string ? true : false; type Test = IsString; // true | false = boolean // To prevent distribution, wrap in a tuple type IsStringExact = [T] extends [string] ? true : false; type Test2 = IsStringExact; // false ``` ### Use infer to Extract Types ```typescript // Extract the element type from an array type UnpackArray = T extends (infer U)[] ? U : T; type Item = UnpackArray; // string type Same = UnpackArray; // number // Extract return type (equivalent to built-in ReturnType) type MyReturnType = T extends (...args: unknown[]) => infer R ? R : never; // Extract the resolved value of a Promise type Awaited = T extends Promise ? Awaited : T; type Value = Awaited>>; // string // Extract first parameter type type FirstParam = T extends (first: infer F, ...rest: unknown[]) => unknown ? F : never; type F = FirstParam<(a: string, b: number) => void>; // string // Extract constructor instance type type InstanceOf = T extends new (...args: unknown[]) => infer I ? I : never; ``` ### Nest Conditional Types for Complex Logic ```typescript type TypeName = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : T extends null ? 'null' : T extends undefined ? 'undefined' : T extends Function ? 'function' : 'object'; type A = TypeName; // 'string' type B = TypeName<() => void>; // 'function' type C = TypeName<{ a: 1 }>; // 'object' // Filter a union: keep only string keys from a type type StringKeys = { [K in keyof T]: T[K] extends string ? K : never; }[keyof T]; interface Mixed { id: string; count: number; name: string; active: boolean; } type OnlyStringFields = StringKeys; // 'id' | 'name' ``` --- ## Mapped Types ### Remap Keys with the as Clause ```typescript // Prefix all keys type Prefixed = { [K in keyof T as `${P}${Capitalize}`]: T[K]; }; interface User { id: string; name: string; } type PrefixedUser = Prefixed; // { userId: string; userName: string } // Filter keys by value type type PickByValue = { [K in keyof T as T[K] extends V ? K : never]: T[K]; }; interface Config { debug: boolean; port: number; host: string; verbose: boolean; } type BooleanConfig = PickByValue; // { debug: boolean; verbose: boolean } ``` ### Apply Modifiers with + and - ```typescript // Add readonly and optional type Immutable = { +readonly [K in keyof T]+?: T[K]; }; // Remove readonly and optional (make mutable and required) type Mutable = { -readonly [K in keyof T]-?: T[K]; }; interface Optional { readonly id?: string; readonly name?: string; } type Concrete = Mutable; // { id: string; name: string } ``` ### Combine Mapped and Conditional Types ```typescript // Make only specific keys optional type MakeOptional = Omit & Partial>; interface Post { id: string; title: string; content: string; publishedAt: Date; } type DraftPost = MakeOptional; // { title: string; content: string; id?: string; publishedAt?: Date } ``` --- ## Template Literal Types in Generics ### Build Type-Safe Event Emitters ```typescript type EventMap = { userCreated: { userId: string }; orderPlaced: { orderId: string; total: number }; sessionExpired: { sessionId: string }; }; type EventListener = (event: TMap[TEvent]) => void; type Emitter = { on( event: TEvent, listener: EventListener ): void; emit(event: TEvent, data: TMap[TEvent]): void; }; declare const emitter: Emitter; emitter.on('userCreated', (e) => console.log(e.userId)); // OK emitter.on('orderPlaced', (e) => console.log(e.total)); // OK emitter.emit('userCreated', { userId: '123' }); // OK emitter.emit('userCreated', { orderId: '123' }); // Error: wrong shape ``` ### Extract Route Parameters ```typescript type RouteParams = T extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param | keyof RouteParams]: string } : T extends `${string}:${infer Param}` ? { [K in Param]: string } : Record; function buildRoute( template: T, params: RouteParams ): string { return Object.entries(params).reduce( (path, [key, value]) => path.replace(`:${key}`, value as string), template ); } const url = buildRoute('/users/:userId/posts/:postId', { userId: '1', postId: '42', }); // '/users/1/posts/42' ``` --- ## Variadic Tuple Types ### Spread Tuples for Function Composition ```typescript // Concatenate two tuple types type Concat = [...T, ...U]; type T1 = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4] // Strongly typed pipe/compose type Pipe unknown)[]> = T extends [infer First, ...infer Rest] ? First extends (...args: infer A) => infer R ? Rest extends [] ? (...args: A) => R : Pipe<[(...args: A) => R, ...Extract unknown)[]>]> : never : never; // Prepend and append to tuples type Prepend = [T, ...Tuple]; type Append = [...Tuple, T]; type WithFirst = Prepend; // [string, number, boolean] type WithLast = Append<[string, number], boolean>; // [string, number, boolean] ``` ### Build Type-Safe curry ```typescript type Head = T extends [infer H, ...unknown[]] ? H : never; type Tail = T extends [unknown, ...infer R] ? R : never; type Curry = TArgs extends [] ? TReturn : (arg: Head) => Curry, TReturn>; declare function curry( fn: (...args: TArgs) => TReturn ): Curry; const add = curry((a: number, b: number, c: number) => a + b + c); const add5 = add(5); // Curry<[number, number], number> const add5and3 = add5(3); // Curry<[number], number> const result = add5and3(2); // number = 10 ``` --- ## Higher-Kinded Types ### Emulate HKT with Interface Lookup TypeScript doesn't natively support higher-kinded types, but they can be emulated with a registry pattern. ```typescript // Define a type-level registry for type constructors interface HKTRegistry { // Registered types go here via module augmentation } type HKT = keyof HKTRegistry; type Apply = HKTRegistry[F] extends { type: unknown } ? (HKTRegistry[F] & { arg: A })['type'] : never; // Register Array as a type constructor declare module './hkt' { interface HKTRegistry { Array: { type: Array }; } } // Functor interface using HKT interface Functor { map(fa: Apply, f: (a: A) => B): Apply; } ``` --- ## Builder Pattern ### Track Builder State in the Type System ```typescript type BuilderState = { hasName: boolean; hasAge: boolean; }; type Builder = { setName(name: string): Builder; setAge(age: number): Builder; } & (State['hasName'] extends true ? State['hasAge'] extends true ? { build(): T } : {} : {}); declare function createBuilder(): Builder<{ hasName: false; hasAge: false }>; const builder = createBuilder(); const user = builder.setName('Alice').setAge(30).build(); // user: { name: string } & { age: number } // Compile errors: // builder.build() - Error: build() not available until required fields set // builder.setName('Alice').build() - Error: age not set ``` ### Use Fluent Interface with Immutable Type Accumulation ```typescript class QueryBuilder = Record> { private conditions: string[] = []; private selectedFields: string[] = []; select(field: K): QueryBuilder> { this.selectedFields.push(field); return this as unknown as QueryBuilder>; } where(condition: string): this { this.conditions.push(condition); return this; } build(): { fields: string[]; conditions: string[] } { return { fields: this.selectedFields, conditions: this.conditions }; } } const query = new QueryBuilder() .select('id') .select('name') .where('active = true') .build(); ``` --- ## Common Generic Patterns ### MaybePromise ```typescript type MaybePromise = T | Promise; async function normalize(value: MaybePromise): Promise { return await value; } ``` ### DeepPartial ```typescript type DeepPartial = T extends (infer U)[] ? DeepPartial[] : T extends object ? { [K in keyof T]?: DeepPartial } : T; ``` ### PathOf and Get ```typescript // PathOf: all dot-notation paths into an object type PathOf = T extends object ? { [K in keyof T]: K extends string ? T[K] extends object ? K | `${K}.${PathOf}` : K : never }[keyof T] : never; // Get: value at a path type Get = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? Get : never : P extends keyof T ? T[P] : never; ``` ### Prettify (Flatten Intersection Types for Readability) ```typescript type Prettify = { [K in keyof T]: T[K] } & {}; type A = { id: string } & { name: string } & { age: number }; type B = Prettify; // { id: string; name: string; age: number } // B displays as a single object in IDE hover, much more readable ``` ### RequireAtLeastOne ```typescript type RequireAtLeastOne = Omit & { [K in Keys]-?: Required> & Partial> }[Keys]; interface ContactOptions { email?: string; phone?: string; address?: string; } type Contact = RequireAtLeastOne; // Must provide at least one of email, phone, or address ``` ### RequireExactlyOne ```typescript type RequireExactlyOne = Omit & { [K in Keys]: Required> & { [O in Exclude]?: never } }[Keys]; interface PaymentMethod { creditCard?: { number: string }; bankTransfer?: { account: string }; paypal?: { email: string }; } type Payment = RequireExactlyOne; // Must provide exactly one payment method ```