|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/generics
$cat docs/typescript-—-generics.md
updated Recently·16 min read·published

TypeScript — Generics

TypeScriptIntermediate
Introduction

Generics allow you to write functions, interfaces, and classes that work with any type while preserving type safety. Instead of using any, generics let you capture the relationship between input and output types — the type flows through your code.

Generic Functions

The identity function is the classic generic example: it takes a value and returns it unchanged, preserving the input type.

identity.ts
TypeScript
1// ❌ Without generics — loses type information
2function identity(arg: any): any {
3 return arg;
4}
5const result = identity("hello"); // type: any (no safety)
6
7// ✅ With generics — type is captured and preserved
8function identityGeneric<T>(arg: T): T {
9 return arg;
10}
11const str = identityGeneric("hello"); // type: string
12const num = identityGeneric(42); // type: number
13const arr = identityGeneric([1, 2]); // type: number[]
14
15// Multiple type parameters
16function pair<A, B>(first: A, second: B): [A, B] {
17 return [first, second];
18}
19const p = pair("hello", 42); // [string, number]
20
21// Generic arrow function
22const wrap = <T>(value: T): { value: T } => ({ value });
23
24// Generic with conditional return
25function first<T>(arr: T[]): T | undefined {
26 return arr[0];
27}
28
29const firstNum = first([1, 2, 3]); // number | undefined
30const firstStr = first(["a", "b"]); // string | undefined
31const firstEmpty = first([]); // undefined
Type Parameter Inference

TypeScript usually infers type parameters from the arguments. You only need explicit type arguments when inference isn't possible or when you want a specific type.

inference.ts
TypeScript
1// Inferred from arguments
2function wrap<T>(value: T): { value: T; timestamp: number } {
3 return { value, timestamp: Date.now() };
4}
5const w = wrap("hello"); // inferred T = string
6
7// Explicit type argument — needed when inference is ambiguous
8function create<T>(value: T): T[] {
9 return [value];
10}
11const nums = create<number>(42); // number[] — explicit
12const mixed = create(42); // number[] — inferred
13
14// Inference from multiple positions
15function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
16 return arr.map(fn);
17}
18// T inferred from arr, U inferred from fn return type
19const lengths = map(["hello", "world"], (s) => s.length);
20// T = string, U = number → number[]
21
22// Inference context — better inference with callbacks
23function reduce<T, U>(
24 arr: T[],
25 fn: (acc: U, item: T) => U,
26 initial: U
27): U {
28 return arr.reduce(fn, initial);
29}
30// U inferred from initial value type
31const total = reduce(
32 [1, 2, 3],
33 (acc, n) => acc + n,
34 0 // initial: 0 → U = number
35);
36
37// When inference fails — provide the type argument
38function merge<T extends object>(a: T, b: Partial<T>): T {
39 return { ...a, ...b };
40}
41const merged = merge({ x: 1, y: 2 }, { y: 10 }); // inferred
42// If inference fails: merge<{ x: number; y: number }>(a, b)

info

TypeScript's inference engine is very capable. Try omitting type arguments first — only add them when the compiler infers unknown or the wrong type.
Generic Constraints (extends)

Constraints restrict which types a generic can accept. Use extends to require that a type parameter satisfies a particular interface or shape.

constraints.ts
TypeScript
1// Constraint: T must have a .length property
2function logLength<T extends { length: number }>(item: T): T {
3 console.log(item.length);
4 return item;
5}
6
7logLength("hello"); // OK — string has .length
8logLength([1, 2, 3]); // OK — array has .length
9// logLength(42); // Error: number doesn't have .length
10
11// Constraint with keyof
12function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
13 return obj[key];
14}
15
16const person = { name: "Alice", age: 30 };
17getProperty(person, "name"); // string
18getProperty(person, "age"); // number
19// getProperty(person, "email"); // Error: "email" not in keyof person
20
21// Chained constraints
22interface HasId {
23 id: number;
24}
25
26interface HasTimestamp {
27 createdAt: Date;
28}
29
30function findById<T extends HasId & HasTimestamp>(
31 items: T[],
32 id: number
33): T | undefined {
34 return items.find((item) => item.id === id);
35}
36
37// Real-world: validating an object has required keys
38function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
39 const result = {} as Pick<T, K>;
40 for (const key of keys) {
41 result[key] = obj[key];
42 }
43 return result;
44}
45
46const user = { name: "Alice", age: 30, email: "a@b.com" };
47const picked = pick(user, ["name", "email"]);
48// { name: string; email: string }
Generic Interfaces
generic_interfaces.ts
TypeScript
1// Generic interface — reusable container
2interface ApiResponse<T> {
3 data: T;
4 status: number;
5 timestamp: Date;
6 error?: string;
7}
8
9const userResponse: ApiResponse<{ name: string; age: number }> = {
10 data: { name: "Alice", age: 30 },
11 status: 200,
12 timestamp: new Date(),
13};
14
15const listResponse: ApiResponse<string[]> = {
16 data: ["a", "b", "c"],
17 status: 200,
18 timestamp: new Date(),
19};
20
21// Generic interface as a map/dictionary
22interface Cache<T> {
23 get(key: string): T | undefined;
24 set(key: string, value: T): void;
25 has(key: string): boolean;
26 delete(key: string): void;
27}
28
29// Generic interface with extends
30interface Paginated<T> {
31 items: T[];
32 total: number;
33 page: number;
34 pageSize: number;
35 hasNext(): boolean;
36}
37
38// Generic type predicate interface
39interface Predicate<T> {
40 (item: T): boolean;
41}
42
43const isPositive: Predicate<number> = (n) => n > 0;
44const isNonEmpty: Predicate<string> = (s) => s.length > 0;
45
46// Generic event emitter
47interface EventEmitter<Events extends Record<string, unknown[]>> {
48 on<K extends keyof Events>(
49 event: K,
50 handler: (...args: Events[K]) => void
51 ): void;
52 emit<K extends keyof Events>(event: K, ...args: Events[K]): void;
53}
54
55type AppEvents = {
56 login: [username: string];
57 logout: [];
58 error: [code: number, message: string];
59};
60
61declare const emitter: EventEmitter<AppEvents>;
62emitter.on("login", (username) => console.log(username)); // string
63emitter.on("error", (code, msg) => {}); // number, string
Generic Classes
generic_classes.ts
TypeScript
1// Generic class — type-safe container
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 get size(): number {
18 return this.items.length;
19 }
20}
21
22const numStack = new Stack<number>();
23numStack.push(1);
24numStack.push(2);
25const top = numStack.pop(); // number | undefined
26
27// Generic class with constraint
28class SortedList<T extends { compareTo(other: T): number }> {
29 private items: T[] = [];
30
31 insert(item: T): void {
32 const idx = this.items.findIndex(
33 (existing) => item.compareTo(existing) < 0
34 );
35 this.items.splice(idx === -1 ? this.items.length : idx, 0, item);
36 }
37
38 getAll(): readonly T[] {
39 return this.items;
40 }
41}
42
43interface Comparable {
44 compareTo(other: Comparable): number;
45}
46
47// Generic class implementing an interface
48class EventEmitter<T extends Record<string, unknown[]>> {
49 private handlers: {
50 [K in keyof T]?: Array<(...args: T[K]) => void>;
51 } = {};
52
53 on<K extends keyof T>(event: K, handler: (...args: T[K]) => void): void {
54 if (!this.handlers[event]) this.handlers[event] = [];
55 this.handlers[event]!.push(handler);
56 }
57
58 emit<K extends keyof T>(event: K, ...args: T[K]): void {
59 this.handlers[event]?.forEach((handler) => handler(...args));
60 }
61}
Generic Default Types

Default type parameters let callers omit the type argument when the default is sufficient. They are commonly used in library APIs and React component types.

defaults.ts
TypeScript
1// Default type parameter
2interface ApiResponse<T = unknown> {
3 data: T;
4 status: number;
5}
6
7// Caller can omit T — defaults to unknown
8const response: ApiResponse = { data: "hello", status: 200 };
9// data is unknown — you must narrow before using
10
11// Caller can provide T for type safety
12const typedResponse: ApiResponse<{ name: string }> = {
13 data: { name: "Alice" },
14 status: 200,
15};
16
17// Multiple defaults
18interface Store<
19 State = Record<string, unknown>,
20 Actions extends Record<string, (...args: any[]) => any> = Record<string, never>
21> {
22 state: State;
23 dispatch: <K extends keyof Actions>(
24 action: K,
25 ...args: Parameters<Actions[K]>
26 ): ReturnType<Actions[K]>;
27}
28
29// Partial defaults — only some parameters specified
30type MyStore = Store<{ count: number }>;
31
32// Real-world: React-style props pattern
33interface ComponentProps<T = Record<string, never>> {
34 children?: React.ReactNode;
35 className?: string;
36 data?: T;
37}
38
39// Simple component — no data prop needed
40const Card: React.FC<ComponentProps> = ({ children, className }) => (
41 <div className={className}>{children}</div>
42);
43
44// Data component — data is typed
45interface User {
46 name: string;
47 email: string;
48}
49
50const UserCard: React.FC<ComponentProps<User>> = ({ data }) => (
51 <div>{data?.name}</div>
52);

info

Default type parameters are especially useful for library authors — they provide sensible defaults while allowing advanced users to customize types.
Generic Utility Patterns
utility_patterns.ts
TypeScript
1// TypeScript provides built-in utility types that use generics
2
3// Partial<T> — all properties optional
4function updateUser<T extends object>(base: T, updates: Partial<T>): T {
5 return { ...base, ...updates };
6}
7
8// Required<T> — all properties required
9type StrictConfig = Required<{ host?: string; port?: number }>;
10
11// Pick<T, K> — select specific properties
12function pickUser<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
13 return keys.reduce(
14 (acc, key) => ({ ...acc, [key]: obj[key] }),
15 {} as Pick<T, K>
16 );
17}
18
19// Omit<T, K> — exclude properties
20type PublicUser = Omit<User, "password" | "salt">;
21
22// Record<K, V> — map with typed keys
23function groupBy<T, K extends string>(
24 items: T[],
25 keyFn: (item: T) => K
26): Record<K, T[]> {
27 return items.reduce((acc, item) => {
28 const key = keyFn(item);
29 if (!acc[key]) acc[key] = [];
30 acc[key].push(item);
31 return acc;
32 }, {} as Record<K, T[]>);
33}
34
35// ReturnType<T> — extract function return type
36function createDefault() {
37 return { name: "", active: true };
38}
39type Default = ReturnType<typeof createDefault>;
40
41// Parameters<T> — extract function parameter types
42function fetchUser(id: number, includePosts: boolean) {
43 return { id, includePosts };
44}
45type FetchArgs = Parameters<typeof fetchUser>; // [number, boolean]
46
47// Extract<T, U> and Exclude<T, U>
48type Numbers = Extract<string | number | boolean, number>; // number
49type NotBool = Exclude<string | number | boolean, boolean>; // string | number
Best Practices

1. Let TypeScript infer type parameters whenever possible. Only provide explicit type arguments when inference fails or you need a specific narrowing.

2. Use meaningful names for type parameters: T for types, K for keys, V for values, E for errors.

3. Constrain generics with extends when you need specific properties — it improves autocompletion and catches errors early.

4. Avoid any in generic positions — it defeats the purpose. Use unknown if you truly need flexibility.

5. Use default type parameters for library APIs — they make simple cases easy while preserving advanced use cases.

6. Compose built-in utility types (Partial, Pick, Omit) instead of rewriting them — they are well-tested and readable.

7. Generic constraints with keyof are powerful for type-safe property access patterns like getProperty<T, K extends keyof T>.

8. Prefer generic functions over any-typed functions — generics preserve the relationship between input and output types.

$Blueprint — Engineering Documentation·Section ID: TS-GEN·Revision: 1.0