泛型与条件类型
泛型与条件类型
写在前面
上一篇讲了联合类型与类型守卫。这一篇讲泛型——TypeScript 类型系统的灵魂。
泛型是 TS 进阶的"分水岭"——会写泛型的人写出来的工具/库强大 10 倍,不会写的人只会用别人写好的。本篇从基础到 infer 关键字,完整覆盖,最后实战实现一个类型安全的 EventEmitter。
一、泛型基础
1.1 什么是泛型
泛型是"参数化的类型"——把类型当作参数,在使用时指定。
// 没有泛型 - 每个类型写一份
function identityString(arg: string): string { return arg; }
function identityNumber(arg: number): number { return arg; }
// 用泛型 - 一个函数搞定
function identity<T>(arg: T): T { return arg; }
const a = identity("hello"); // a 是 string
const b = identity(42); // b 是 number
const c = identity<User>({...}); // 显式指定
1.2 泛型函数
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first([1, 2, 3]); // number | undefined
first(["a", "b"]); // string | undefined
first([]); // undefined
1.3 泛型 interface
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
1.4 泛型 type
type Pair<K, V> = {
key: K;
value: V;
};
const pair: Pair<string, number> = { key: "age", value: 30 };
1.5 泛型类
class Container<T> {
constructor(public value: T) {}
map<U>(fn: (value: T) => U): Container<U> {
return new Container(fn(this.value));
}
}
const c = new Container(42);
const s = c.map(n => n.toString()); // Container<string>
二、泛型约束
2.1 基础约束
// 错误 - T 没有 length 属性
function logLength<T>(arg: T): void {
console.log(arg.length); // 报错
}
// 正确 - 用 extends 约束 T 至少有 length
function logLength<T extends { length: number }>(arg: T): void {
console.log(arg.length);
}
logLength("hello"); // OK
logLength([1, 2, 3]); // OK
logLength({ length: 5 }); // OK
logLength(42); // 报错 - number 没有 length
2.2 keyof 约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, email: "alice@example.com" };
getProperty(user, "name"); // string
getProperty(user, "age"); // number
getProperty(user, "foo"); // 报错 - 'foo' 不是 user 的 key
T[K] 是索引访问类型——返回该 key 对应的值类型。
2.3 条件约束
// T 必须是对象
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
// T 必须是 string | number | boolean
function process<T extends string | number | boolean>(value: T): T {
return value;
}
2.4 默认类型参数
interface ApiResponse<T = unknown, E = Error> {
data?: T;
error?: E;
}
const r1: ApiResponse = { data: { ... } }; // T 是 unknown
const r2: ApiResponse<User> = { data: { ... } }; // T 是 User
const r3: ApiResponse<User, NetworkError> = { ... }; // 都指定
三、条件类型
3.1 基础语法
T extends U ? X : Y
T 是 U 的子类型吗?是的话是 X,不是的话是 Y。
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true(字面量是 string 的子类型)
3.2 条件类型的实际意义
条件类型让类型可以"分支"——根据输入类型决定输出类型。
// 把数组转成元素类型,其他保持不变
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<number[]>; // number
type B = Flatten<string>; // string
type C = Flatten<boolean[]>; // boolean
infer U 表示"推断出数组元素类型"——见下一节详解。
3.3 实战:提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<User>>; // User
type C = UnwrapPromise<number>; // number
这个模式叫 Awaited<T>——TypeScript 内置了,效果一样。
四、infer 关键字
4.1 什么是 infer
infer 在条件类型的 extends 子句中,声明一个"待推断"的类型变量。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function foo(): number { return 42; }
type FooReturn = ReturnType<typeof foo>; // number
TypeScript 推断出函数返回类型 R,作为条件类型的结果。
4.2 实战:提取函数参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number): void {}
type GreetParams = Parameters<typeof greet>; // [name: string, age: number]
// 取第一个参数
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type GreetFirst = FirstParam<typeof greet>; // string
内置 Parameters<T> 跟这个一样。
4.3 实战:提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : T;
type A = ElementOf<string[]>; // string
type B = ElementOf<number[]>; // number
type C = ElementOf<boolean>; // boolean
内置 Array<T> 反过来用即可:T[number]。
4.4 实战:实现类型安全的 EventEmitter
type EventMap = {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"cart:add": { productId: string; quantity: number };
"error": Error;
};
class TypedEmitter<E extends Record<string, any>> {
private listeners: { [K in keyof E]?: Array<(data: E[K]) => void> } = {};
on<K extends keyof E>(event: K, listener: (data: E[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
off<K extends keyof E>(event: K, listener: (data: E[K]) => void): void {
const arr = this.listeners[event];
if (arr) {
this.listeners[event] = arr.filter(l => l !== listener);
}
}
emit<K extends keyof E>(event: K, data: E[K]): void {
this.listeners[event]?.forEach(l => l(data));
}
}
// 用法
const emitter = new TypedEmitter<EventMap>();
emitter.on("user:login", (data) => {
// data 是 { userId: string; timestamp: number }
console.log(data.userId, data.timestamp);
});
emitter.emit("user:login", { userId: "123", timestamp: Date.now() });
emitter.emit("user:login", { userId: 123 }); // 报错!userId 应该是 string
emitter.emit("foo", {}); // 报错!"foo" 不是合法事件
每个事件的参数类型都精确——这就是类型安全。
五、分布式条件类型
5.1 什么是分布式条件类型
当泛型是裸类型参数(裸 T,没有包装),且是联合类型时,条件类型会自动分布到每个成员。
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
// 不是 (string | number)[]
// 而是 string[] | number[]
// 等价于:
type A = string[] | number[];
这是"分布"——条件类型对联合类型的每个成员分别求值。
5.2 用方括号禁止分布
// 禁止分布 - 整个联合类型作为一个整体
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type A = ToArrayNonDist<string | number>;
// 是 (string | number)[]
5.3 实战:Exclude 和 Extract
// 内置 Exclude - 排除 T 中是 U 的子类型的成员
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
// 内置 Extract - 提取 T 中是 U 的子类型的成员
type Extract<T, U> = T extends U ? T : never;
type B = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
这两个都依赖分布式条件类型——没有分布就没法用。
5.4 实战:NonNullable
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null | undefined>; // string
type B = NonNullable<number | null>; // number
5.5 实战:filter 工具
// 提取 T 中有特定属性的成员
type WithProperty<T, K extends string> = T extends { [P in K]: any } ? T : never;
interface User { type: "user"; name: string }
interface Admin { type: "admin"; name: string; permissions: string[] }
interface Product { type: "product"; sku: string }
type WithName = WithProperty<User | Admin | Product, "name">; // User | Admin
type WithPermissions = WithProperty<User | Admin | Product, "permissions">; // Admin
六、映射类型(Mapped Types)
6.1 基础映射
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
interface User { name: string; age: number; }
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; }
6.2 映射修饰符
// 添加/删除 readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// 添加/删除 optional
type Required<T> = {
[K in keyof T]-?: T[K];
};
+ 和 - 修饰符——+ 加,- 减。
6.3 实战:类型安全的 API client
// 全部转成可选
type PartialUpdate<T> = Partial<T>;
// 全部转成 nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// 全部转成 promise
type Promisify<T> = {
[K in keyof T]: Promise<T[K]>;
};
6.4 实战:从 enum-like 联合类型生成映射
type Status = "pending" | "success" | "error";
// 从联合类型生成对象类型
type StatusMap = {
[K in Status]: { value: K; label: string };
};
// {
// pending: { value: "pending"; label: string };
// success: { value: "success"; label: string };
// error: { value: "error"; label: string };
// }
七、内置工具类型
7.1 常用工具
// 对象相关
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type Record<K extends keyof any, T> = { [P in K]: T };
// 联合类型
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type NonNullable<T> = T extends null | undefined ? never : T;
// 函数
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Awaited<T> = T extends Promise<infer U> ? U : T;
// 字符串
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
7.2 Pick 和 Omit 实战
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
}
// 只需要 name 和 email
type PublicUser = Pick<User, "id" | "name" | "email">;
// 不要 password
type SafeUser = Omit<User, "password">;
7.3 Record 实战
// 枚举映射
const statusLabels: Record<"pending" | "success" | "error", string> = {
pending: "处理中",
success: "成功",
error: "失败",
};
八、类型体操的几个常见套路
8.1 套路 1:从函数提取参数
type FirstParameter<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
function fn(name: string, age: number, active: boolean) {}
type First = FirstParameter<typeof fn>; // string
8.2 套路 2:从数组提取第一个
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type A = First<[1, 2, 3]>; // 1
type B = First<[]>; // never
8.3 套路 3:从对象提取特定 key 的值类型
type ValueOf<T, K extends keyof T> = T[K];
interface User { name: string; age: number; }
type Age = ValueOf<User, "age">; // number
8.4 套路 4:深度 Readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Config {
server: { host: string; port: number };
database: { url: string };
}
type Frozen = DeepReadonly<Config>;
// {
// readonly server: { readonly host: string; readonly port: number };
// readonly database: { readonly url: string };
// }
8.5 套路 5:对象转联合
type ObjectToUnion<T> = T[keyof T];
interface User { name: string; age: number; }
type Values = ObjectToUnion<User>; // string | number
8.6 套路 6:字符串转联合
type Split<S extends string, Sep extends string = ""> =
S extends `${infer First}${Sep}${infer Rest}`
? First | Split<Rest, Sep>
: S;
type Path = Split<"users.posts.comments", ".">;
// "users" | "posts" | "comments"
九、实战:类型安全的 API 客户端
9.1 定义 API 端点
type ApiEndpoints = {
"/users": {
GET: { response: User[]; query?: { limit?: number } };
POST: { body: CreateUserInput; response: User };
};
"/users/:id": {
GET: { params: { id: string }; response: User };
DELETE: { params: { id: string }; response: void };
};
"/orders": {
GET: { response: Order[] };
POST: { body: CreateOrderInput; response: Order };
};
};
9.2 类型安全的请求函数
type Method = keyof ApiEndpoints["/users"]; // "GET" | "POST"
type PathParams<T extends string> =
T extends `${string}:${infer P}/${infer R}` ? P | PathParams<R> :
T extends `${string}:${infer P}` ? P :
never;
async function request<
P extends keyof ApiEndpoints,
M extends keyof ApiEndpoints[P]
>(
method: M,
path: P,
options: ApiEndpoints[P][M] extends { body: infer B } ? { body: B } :
ApiEndpoints[P][M] extends { params: infer P } ? { params: P } :
ApiEndpoints[P][M] extends { query: infer Q } ? { query: Q } :
{} = {} as any
): Promise<ApiEndpoints[P][M] extends { response: infer R } ? R : void> {
// 实际请求逻辑
return fetch(...).then(r => r.json());
}
// 用法
const users = await request("GET", "/users");
// users 是 User[]
const user = await request("GET", "/users/:id", { params: { id: "123" } });
// user 是 User
十、本篇小结
| 概念 | 关键点 |
|---|---|
| 泛型 | 参数化的类型 |
| 约束 | T extends X |
| 条件类型 | T extends U ? X : Y |
infer |
在条件类型中推断子类型 |
| 分布式 | 裸 T 对联合类型自动分布 |
| 映射类型 | 批量转换对象属性 |
| 实战 | 应用 |
|---|---|
| EventEmitter | 事件名 + payload 类型精确 |
| API 客户端 | 端点 + 方法 + 参数类型精确 |
| 状态机 | 不同状态不同 payload |
| 工具库 | 类型安全的 helper |
下一篇讲 模板字面量类型——TS 4.1+ 的字符串类型魔法,把路由路径提取成类型。