Agent X-Ray
NotesSkillsAbout
Notes/代码工程/TypeScript 深度教程/第7章

第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>>;

这体现“先映射每个键,再索引得到联合”的常见模式。

十、类型级设计的边界

好的高级类型应满足:

  1. 减少重复定义。
  2. 错误信息对调用者仍可理解。
  3. 类型名称表达业务含义。
  4. 不依赖脆弱的编译器边缘行为。
  5. 编译时间和编辑器响应可接受。

当类型表达式超过团队可维护阈值时,可考虑:

  • 分解成多个命名类型。
  • 在公共 API 暴露简化类型。
  • 通过代码生成生成声明。
  • 在运行时使用 schema,再从 schema 推导静态类型。

十一、常见误区

  1. 手工复制 DTO,而不是从稳定源派生。
  2. Partial<T> 被当作所有更新模型。
  3. 不理解条件类型的分布行为。
  4. 自制工具类型重复内置能力。
  5. 深递归类型让编辑器和 CI 变慢。
  6. 把所有字符串协议塞进模板字面量类型。
  7. 公共 API 暴露巨大条件类型,错误难读。
  8. 高级类型没有类型测试,升级后悄悄变化。

十二、实践练习

  1. 实现安全的 getProperty
  2. User 派生公开字段和更新命令,但不要直接使用全量 Partial<User>
  3. 实现 StringKeys<T>OnlyStringFields<T>
  4. 比较分布式与非分布式条件类型。
  5. infer 提取函数返回值和 Promise 内部类型。
  6. 为类型工具编写正确和错误用例。

十三、总结

  1. keyof 和索引访问类型连接对象键与属性类型。
  2. 映射类型批量变换属性,内置工具类型覆盖大多数常见需求。
  3. 条件类型执行类型级分支,联合分布行为必须明确。
  4. infer 用于从类型结构中提取部分。
  5. 模板字面量类型适合有限、可预测的字符串协议。
  6. 高级类型应减少漂移,而不是制造只有作者能理解的谜题。

请继续阅读:第8章:类的高级能力。


原始资料引用


  • 第6章:模块化工程
  • 第8章:类的高级能力

本章目录
一、从 keyof 开始二、索引访问类型三、映射类型四、内置工具类型五、条件类型六、infer七、模板字面量类型八、递归工具类型九、never 在高级类型中的作用十、类型级设计的边界十一、常见误区十二、实践练习十三、总结原始资料引用Related Documents
苏ICP备2025204887号-2