Generics enable flexible, reusable code while maintaining type safety. Here's how to use advanced generic patterns effectively.
Generic Constraints#
1// Basic constraint
2function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
3 return obj[key];
4}
5
6const user = { name: 'John', age: 30 };
7getProperty(user, 'name'); // string
8getProperty(user, 'age'); // number
9getProperty(user, 'foo'); // Error: 'foo' is not in keyof user
10
11// Multiple constraints with intersection
12interface HasId {
13 id: string;
14}
15
16interface HasTimestamp {
17 createdAt: Date;
18}
19
20function logEntity<T extends HasId & HasTimestamp>(entity: T) {
21 console.log(`${entity.id} created at ${entity.createdAt}`);
22}
23
24// Constructor constraint
25type Constructor<T> = new (...args: any[]) => T;
26
27function createInstance<T>(ctor: Constructor<T>, ...args: any[]): T {
28 return new ctor(...args);
29}
30
31class User {
32 constructor(public name: string) {}
33}
34
35const user = createInstance(User, 'John'); // User type
36
37// Constraint with default
38interface PaginationOptions<T = unknown> {
39 items: T[];
40 page: number;
41 pageSize: number;
42}Generic Inference#
1// Infer from function arguments
2function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
3 return arr.map(fn);
4}
5
6// T and U are inferred
7const lengths = map(['a', 'bb', 'ccc'], (s) => s.length);
8// number[]
9
10// Infer from return type
11function createPair<T, U>(first: T, second: U): [T, U] {
12 return [first, second];
13}
14
15const pair = createPair('hello', 42); // [string, number]
16
17// Infer array element type
18function first<T>(arr: T[]): T | undefined {
19 return arr[0];
20}
21
22const firstNum = first([1, 2, 3]); // number | undefined
23
24// Infer with conditional types
25type ArrayElement<T> = T extends (infer E)[] ? E : never;
26
27type Num = ArrayElement<number[]>; // number
28type Str = ArrayElement<string[]>; // string
29
30// Infer function parameter types
31type Parameters<T extends (...args: any) => any> =
32 T extends (...args: infer P) => any ? P : never;
33
34type Params = Parameters<(a: string, b: number) => void>;
35// [a: string, b: number]Generic Classes#
1// Generic collection
2class Stack<T> {
3 private items: T[] = [];
4
5 push(item: T): void {
6 this.items.push(item);
7 }
8
9 pop(): T | undefined {
10 return this.items.pop();
11 }
12
13 peek(): T | undefined {
14 return this.items[this.items.length - 1];
15 }
16
17 isEmpty(): boolean {
18 return this.items.length === 0;
19 }
20}
21
22const numberStack = new Stack<number>();
23numberStack.push(1);
24numberStack.push(2);
25numberStack.pop(); // number | undefined
26
27// Generic class with constraints
28class Repository<T extends { id: string }> {
29 private items: Map<string, T> = new Map();
30
31 add(item: T): void {
32 this.items.set(item.id, item);
33 }
34
35 get(id: string): T | undefined {
36 return this.items.get(id);
37 }
38
39 getAll(): T[] {
40 return Array.from(this.items.values());
41 }
42
43 delete(id: string): boolean {
44 return this.items.delete(id);
45 }
46}
47
48interface User {
49 id: string;
50 name: string;
51}
52
53const userRepo = new Repository<User>();
54userRepo.add({ id: '1', name: 'John' });
55
56// Static members with generics
57class StaticGeneric {
58 static create<T>(value: T): Box<T> {
59 return new Box(value);
60 }
61}
62
63class Box<T> {
64 constructor(public value: T) {}
65}
66
67const box = StaticGeneric.create(42); // Box<number>Conditional Types#
1// Basic conditional type
2type IsString<T> = T extends string ? true : false;
3
4type A = IsString<string>; // true
5type B = IsString<number>; // false
6
7// Distributive conditional types
8type ToArray<T> = T extends any ? T[] : never;
9
10type StringOrNumberArray = ToArray<string | number>;
11// string[] | number[]
12
13// Prevent distribution with tuple
14type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
15
16type Together = ToArrayNonDist<string | number>;
17// (string | number)[]
18
19// Extract and Exclude
20type Extract<T, U> = T extends U ? T : never;
21type Exclude<T, U> = T extends U ? never : T;
22
23type Numbers = Extract<string | number | boolean, number>;
24// number
25
26type NotNumbers = Exclude<string | number | boolean, number>;
27// string | boolean
28
29// Infer in conditional types
30type Flatten<T> = T extends Array<infer U> ? U : T;
31
32type Flattened = Flatten<string[]>; // string
33type NotFlattened = Flatten<string>; // string
34
35// Multiple inferences
36type UnpackPromise<T> = T extends Promise<infer U>
37 ? U extends Promise<infer V>
38 ? V
39 : U
40 : T;
41
42type Result = UnpackPromise<Promise<Promise<string>>>; // stringMapped Types with Generics#
1// Make all properties optional
2type Partial<T> = {
3 [P in keyof T]?: T[P];
4};
5
6// Make all properties required
7type Required<T> = {
8 [P in keyof T]-?: T[P];
9};
10
11// Make all properties readonly
12type Readonly<T> = {
13 readonly [P in keyof T]: T[P];
14};
15
16// Remove readonly
17type Mutable<T> = {
18 -readonly [P in keyof T]: T[P];
19};
20
21// Pick specific keys
22type Pick<T, K extends keyof T> = {
23 [P in K]: T[P];
24};
25
26// Omit specific keys
27type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
28
29// Transform property types
30type Stringify<T> = {
31 [P in keyof T]: string;
32};
33
34// Key remapping (TypeScript 4.1+)
35type Getters<T> = {
36 [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
37};
38
39interface Person {
40 name: string;
41 age: number;
42}
43
44type PersonGetters = Getters<Person>;
45// { getName: () => string; getAge: () => number; }
46
47// Filter keys by value type
48type FilterByType<T, ValueType> = {
49 [K in keyof T as T[K] extends ValueType ? K : never]: T[K];
50};
51
52interface Mixed {
53 name: string;
54 age: number;
55 active: boolean;
56}
57
58type StringProps = FilterByType<Mixed, string>;
59// { name: string }Generic Utility Functions#
1// Type-safe object entries
2function entries<T extends object>(obj: T): [keyof T, T[keyof T]][] {
3 return Object.entries(obj) as [keyof T, T[keyof T]][];
4}
5
6// Type-safe Object.keys
7function keys<T extends object>(obj: T): (keyof T)[] {
8 return Object.keys(obj) as (keyof T)[];
9}
10
11// Type-safe Object.fromEntries
12function fromEntries<K extends string, V>(
13 entries: [K, V][]
14): Record<K, V> {
15 return Object.fromEntries(entries) as Record<K, V>;
16}
17
18// Deep clone with type preservation
19function deepClone<T>(obj: T): T {
20 if (obj === null || typeof obj !== 'object') {
21 return obj;
22 }
23
24 if (Array.isArray(obj)) {
25 return obj.map(deepClone) as unknown as T;
26 }
27
28 return Object.fromEntries(
29 Object.entries(obj).map(([key, value]) => [key, deepClone(value)])
30 ) as T;
31}
32
33// Type-safe merge
34function merge<T extends object, U extends object>(
35 target: T,
36 source: U
37): T & U {
38 return { ...target, ...source };
39}Variadic Generics#
1// Tuple types with rest elements
2type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
3
4type Result = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]
5
6// Function with variadic generics
7function concat<T extends unknown[], U extends unknown[]>(
8 arr1: T,
9 arr2: U
10): [...T, ...U] {
11 return [...arr1, ...arr2];
12}
13
14const result = concat([1, 2], ['a', 'b']);
15// [number, number, string, string]
16
17// Curry with type preservation
18type Curry<F extends (...args: any[]) => any> = F extends (
19 arg: infer A,
20 ...rest: infer Rest
21) => infer R
22 ? Rest extends []
23 ? F
24 : (arg: A) => Curry<(...args: Rest) => R>
25 : never;
26
27function curry<F extends (...args: any[]) => any>(fn: F): Curry<F> {
28 return function curried(...args: any[]): any {
29 if (args.length >= fn.length) {
30 return fn(...args);
31 }
32 return (...more: any[]) => curried(...args, ...more);
33 } as Curry<F>;
34}
35
36const add = (a: number, b: number, c: number) => a + b + c;
37const curriedAdd = curry(add);
38
39curriedAdd(1)(2)(3); // 6Real-World Patterns#
1// Event emitter with typed events
2type EventMap = {
3 click: { x: number; y: number };
4 focus: { target: HTMLElement };
5 submit: { data: FormData };
6};
7
8class TypedEventEmitter<T extends Record<string, any>> {
9 private listeners: Partial<{ [K in keyof T]: Set<(data: T[K]) => void> }> = {};
10
11 on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
12 if (!this.listeners[event]) {
13 this.listeners[event] = new Set();
14 }
15 this.listeners[event]!.add(callback);
16 }
17
18 emit<K extends keyof T>(event: K, data: T[K]): void {
19 this.listeners[event]?.forEach((callback) => callback(data));
20 }
21}
22
23const emitter = new TypedEventEmitter<EventMap>();
24emitter.on('click', ({ x, y }) => console.log(x, y));
25emitter.emit('click', { x: 10, y: 20 });
26
27// Builder pattern with generics
28class Builder<T extends object = {}> {
29 private obj: T;
30
31 constructor(obj: T = {} as T) {
32 this.obj = obj;
33 }
34
35 with<K extends string, V>(
36 key: K,
37 value: V
38 ): Builder<T & { [key in K]: V }> {
39 return new Builder({ ...this.obj, [key]: value } as T & { [key in K]: V });
40 }
41
42 build(): T {
43 return this.obj;
44 }
45}
46
47const result = new Builder()
48 .with('name', 'John')
49 .with('age', 30)
50 .with('active', true)
51 .build();
52// { name: string; age: number; active: boolean }Best Practices#
Design:
✓ Use constraints to narrow generic types
✓ Let TypeScript infer when possible
✓ Provide defaults for common cases
✓ Use meaningful type parameter names
Performance:
✓ Avoid deeply nested generics
✓ Simplify conditional types when possible
✓ Use mapped types for transformations
✓ Consider compile-time cost
Readability:
✓ Document complex generic types
✓ Break down complex types into smaller ones
✓ Use type aliases for clarity
✓ Test types with explicit annotations
Conclusion#
Advanced generics enable powerful, reusable abstractions while maintaining type safety. Master constraints for narrowing types, conditional types for branching logic, and mapped types for transformations. The key is balancing flexibility with readability - complex generic types should be well-documented and tested.