第7章:高级类型 —— 工具类型、条件类型、映射、索引与 infer
约 3 分钟 · 更新于 2026-09-01
第7章:高级类型 —— 工具类型、条件类型、映射、索引与 infer
高级类型的目标是从已有契约派生新契约,减少重复和漂移。判断标准不是“写得多聪明”,而是调用者能否更安全、维护者能否读懂、编译器能否高效处理。
一、从 keyof 开始
ts
interface User {
id: string;
name: string;
active: boolean;
}
type UserKey = keyof User; // "id" | "name" | "active"
结合泛型安全读取属性:
ts
function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
const user = { id: "1", name: "Ada", active: true };
const name = getProperty(user, "name"); // string
二、索引访问类型
ts
type UserName = User["name"];
type UserValue = User[keyof User];
数组元素类型:
ts
const cabins = ["economy", "business"] as const;
type Cabin = (typeof cabins)[number];
这类写法让类型直接从真实定义派生,避免手工同步。
三、映射类型
ts
type Optional<T> = {
[K in keyof T]?: T[K];
};
type ReadonlyModel<T> = {
readonly [K in keyof T]: T[K];
};
移除修饰符:
ts
type MutableRequired<T> = {
-readonly [K in keyof T]-?: T[K];
};
键重映射:
ts
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
四、内置工具类型
| 工具类型 | 用途 |
|---|
| Partial<T> | 所有属性可选 |
| Required<T> | 所有属性必选 |
| Readonly<T> | 所有属性只读 |
| Pick<T, K> | 选择属性 |
| Omit<T, K> | 排除属性 |
| Record<K, V> | 从键集合创建记录 |
| Exclude<U, M> | 从联合排除成员 |
| Extract<U, M> | 从联合提取成员 |
| NonNullable<T> | 排除 null/undefined |
| Parameters<F> | 获取函数参数元组 |
| ReturnType<F> | 获取函数返回类型 |
| Awaited<T> | 解包 Promise 类结构 |
示例:
ts
interface User {
id: string;
name: string;
passwordHash: string;
}
type PublicUser = Omit<User, "passwordHash">;
type UserPatch = Partial<Pick<User, "name">>;
不要把 Partial<Entity> 直接当作所有更新接口;数据库实体与更新命令通常有不同业务规则。
五、条件类型
ts
type IsString<T> = T extends string ? true : false;
从数组取元素:
ts
type ElementType<T> = T extends readonly (infer U)[] ? U : T;
条件类型对裸类型参数的联合会分布:
ts
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]
阻止分布可包进元组:
ts
type ToArrayWhole<T> = [T] extends [unknown] ? T[] : never;
六、infer
infer 只能在条件类型中引入待推断变量:
ts
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type MyParameters<T> = T extends (...args: infer P) => unknown ? P : never;
Promise 解包:
ts
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
实际项目优先使用内置 Awaited<T>,除非需要定制规则。
七、模板字面量类型
ts
type EventName = "created" | "updated";
type UserEvent = `user:${EventName}`;
从字段派生事件 API:
ts
type ChangeEvents<T> = {
[K in keyof T as `${string & K}Changed`]: (value: T[K]) => void;
};
适合路由、事件名、CSS 属性或协议字符串。不要尝试用类型系统完整解析任意复杂文本协议。
八、递归工具类型
ts
type DeepReadonly<T> =
T extends (...args: any[]) => unknown ? T :
T extends readonly unknown[] ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
T;
递归工具需要明确:
- 函数是否保持原样。
- 数组、元组、Map、Set 如何处理。
- Date 等特殊对象是否应递归。
- 深度与编译性能是否可接受。
通用 DeepPartial 往往比想象中更有业务风险。
九、never 在高级类型中的作用
映射类型中用 never 过滤键:
ts
type StringKeys<T> = {
[K in keyof T]-?: T[K] extends string ? K : never
}[keyof T];
ts
type OnlyStringFields<T> = Pick<T, StringKeys<T>>;
这体现“先映射每个键,再索引得到联合”的常见模式。
十、类型级设计的边界
好的高级类型应满足:
- 减少重复定义。
- 错误信息对调用者仍可理解。
- 类型名称表达业务含义。
- 不依赖脆弱的编译器边缘行为。
- 编译时间和编辑器响应可接受。
当类型表达式超过团队可维护阈值时,可考虑:
- 分解成多个命名类型。
- 在公共 API 暴露简化类型。
- 通过代码生成生成声明。
- 在运行时使用 schema,再从 schema 推导静态类型。
十一、常见误区
- 手工复制 DTO,而不是从稳定源派生。
- Partial<T> 被当作所有更新模型。
- 不理解条件类型的分布行为。
- 自制工具类型重复内置能力。
- 深递归类型让编辑器和 CI 变慢。
- 把所有字符串协议塞进模板字面量类型。
- 公共 API 暴露巨大条件类型,错误难读。
- 高级类型没有类型测试,升级后悄悄变化。
十二、实践练习
- 实现安全的 getProperty。
- 从 User 派生公开字段和更新命令,但不要直接使用全量 Partial<User>。
- 实现 StringKeys<T> 和 OnlyStringFields<T>。
- 比较分布式与非分布式条件类型。
- 用 infer 提取函数返回值和 Promise 内部类型。
- 为类型工具编写正确和错误用例。
十三、总结
- keyof 和索引访问类型连接对象键与属性类型。
- 映射类型批量变换属性,内置工具类型覆盖大多数常见需求。
- 条件类型执行类型级分支,联合分布行为必须明确。
- infer 用于从类型结构中提取部分。
- 模板字面量类型适合有限、可预测的字符串协议。
- 高级类型应减少漂移,而不是制造只有作者能理解的谜题。
请继续阅读:第8章:类的高级能力。
原始资料引用