TypeScript Generics: Practical Patterns for Real Code

TypeScript generics are one of the type system’s most powerful features — and one of the most frequently misunderstood. Many developers either avoid generics (writing `any` instead) or over-engineer with generics they do not need. This guide focuses on the practical patterns that appear in everyday TypeScript code and the specific problems they solve.

What Generics Are and When You Need Them

The core problem generics solve: writing functions that work across multiple types while preserving type information. Without generics:
“`typescript
function identity(value: any): any { return value; }
const result = identity(42); // result: any — type information lost
“`
With generics:
“`typescript
function identity(value: T): T { return value; }
const result = identity(42); // result: number — type preserved
“`
The `T` is a type parameter — a placeholder for a type that will be specified (or inferred) at call time. When to use generics: when you need a function or class that behaves consistently across types while preserving type information; when you are building reusable utility types; when the return type of a function depends on the input type. When NOT to use generics: when `unknown` (type-checked but unknown) or a union type (e.g., `string | number`) would suffice; when the types are always the same. Over-generics is a common antipattern — if a function only ever takes and returns strings, it should not be generic. The most important generic patterns: generic functions (as above), generic interfaces, generic type aliases, and generic constraints.

Practical Generic Patterns

Generic constraints (`extends`): constrain what types a generic can be. The most common pattern:
“`typescript
function getProperty(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: “Alice”, age: 30 };
const name = getProperty(user, “name”); // string — autocomplete works
// getProperty(user, “email”) would error at compile time
“`
This is the `keyof` pattern — TypeScript’s way of expressing “a string that is one of the keys of type T”. The return type `T[K]` is a “indexed access type” — TypeScript resolves it to the actual property type. Generic interfaces for data structures:
“`typescript
interface ApiResponse {
data: T;
status: number;
message: string;
}
type UserResponse = ApiResponse; // data: User
type PostsResponse = ApiResponse; // data: Post[]
“`
This pattern is ubiquitous in API client code. The `Awaited` utility type unwraps Promises: `Awaited>` = `string`. Conditional types with generics:
“`typescript
type IsArray = T extends any[] ? true : false;
type A = IsArray; // true
type B = IsArray; // false
“`
The `infer` keyword in conditional types:
“`typescript
type UnpackPromise = T extends Promise ? U : T;
type Unpacked = UnpackPromise>; // string
type Direct = UnpackPromise; // number
“`
The `infer U` says “if T is a Promise, infer the wrapped type into U”. This is how utility types like `Awaited`, `ReturnType`, and `Parameters` work internally. Mapped types (transforming every key in a type):
“`typescript
type Partial = { [K in keyof T]?: T[K] }; // built-in
type Readonly = { readonly [K in keyof T]: T[K] }; // built-in
type Stringify = { [K in keyof T]: string };
“`
The built-in utility types (`Partial`, `Required`, `Readonly`, `Pick`, `Omit`, `Record`, `ReturnType`, `Parameters`) are all implemented with generics and mapped/conditional types internally — understanding them is the key to reading complex TypeScript type signatures.

上一篇 意大利咖啡文化:浓缩咖啡、咖啡吧和规则
下一篇 TypeScript泛型:真实代码的实用模式