2. TypeScript类型系统深度解析
TypeScript 的类型系统并不是“给 JavaScript 加注解”这么简单,它本质上是一套可计算的类型语言。基础类型解决的是“这个值是什么”,高级类型解决的是“类型之间如何组合、推导与约束”。在企业级项目中,真正拉开代码质量差距的,往往不是会不会写 interface,而是能否用联合、交叉、条件、映射和模板字面量类型,把业务约束前移到编译期。
一、基础类型是类型系统的地基
基础类型包括 string、number、boolean、null、undefined、symbol、bigint、object、unknown、never、void 以及字面量类型。工程中最容易被忽视的是 unknown 与 any 的区别:any 会关闭类型检查,unknown 则要求先收窄再使用。例如处理接口返回值时:
function parse(input: unknown) {
if (typeof input === "string") {
return input.trim(); // 此处 input 被收窄为 string
}
if (typeof input === "number") {
return input.toFixed(2);
}
return "";
}
字面量类型则让“值”本身成为类型,例如 type Status = "pending" | "success" | "failed",这为后续联合类型和模板字面量类型打下基础。
二、联合与交叉:组合的两个方向
联合类型 A | B 表示“或”,交叉类型 A & B 表示“且”。联合类型常用于状态、事件、可辨识联合;交叉类型常用于混入(mixin)和对象能力叠加。可辨识联合是工程中最实用的模式之一:
type Result =
| { kind: "ok"; data: string[] }
| { kind: "error"; message: string; code: number };
function handle(r: Result) {
switch (r.kind) {
case "ok":
return r.data.length; // 收窄为 ok 分支
case "error":
return r.code; // 收窄为 error 分支
}
}
交叉类型要注意冲突:{ a: string } & { a: number } 会得到 never,因为同一属性无法同时满足两种类型。工程中应避免无意义的交叉,而用它表达“组合能力”,例如 type UserWithTimestamps = User & Timestamps。
三、条件类型与 infer:让类型参与判断
条件类型形如 T extends U ? X : Y,它让类型具备分支能力。infer 则用于在条件类型中“提取”类型变量。典型场景是从函数类型中提取返回值:
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = (id: number) => Promise<string>;
type R = ReturnOf<Fn>; // Promise<string>
条件类型还支持分布式行为:当 T 是裸类型参数且为联合类型时,会对每个成员分别求值。例如 Exclude<T, U> 正是基于此实现。工程中常用它做类型过滤,例如从事件联合中排除内部事件。
四、映射类型:批量改造对象结构
映射类型通过 [K in keyof T] 遍历属性并生成新类型。内置的 Partial、Required、Readonly、Pick、Omit 都是映射类型的产物。实际项目中,我们常自定义映射来统一处理字段:
type Nullable<T> = { [K in keyof T]: T[K] | null };
interface User { id: number; name: string; email: string; }
type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email: string | null }
映射类型还可配合 as 做键重映射,例如把 onClick 转为 onclick,或为所有字段加前缀。这在生成 API 请求参数、表单字段类型时非常高效。
五、模板字面量类型:把字符串约束进类型
模板字面量类型用反引号语法在类型层面拼接字符串,常与联合类型组合生成有限集合。例如定义事件名:
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
它还能配合 infer 做字符串解析,例如从 "user:created" 中提取 "user" 与 "created"。在路由、权限码、CSS 变量、i18n key 等场景中,模板字面量类型能显著减少拼写错误。
六、工程化落地:类型即约束,约束即文档
把上述能力组合起来,可以构建一套“类型驱动”的工程约束。下面这张图展示了一个典型的企业级类型处理链:从基础类型出发,经过联合/交叉组合,再由条件类型做判断,映射类型做结构改造,模板字面量类型做字符串约束,最终输出可被业务代码安全消费的类型。
在实际项目中,建议遵循三条原则。第一,优先使用联合类型表达“有限状态”,而不是用多个布尔字段拼状态,这样能避免非法状态组合。第二,条件类型和映射类型应封装为可复用的工具类型,放在 types/ 目录统一维护,避免散落在业务文件中。第三,模板字面量类型适合约束“有规律的字符串”,但不要过度使用导致类型报错难以阅读;当类型复杂度超过可维护阈值时,应退回到运行时校验或更简单的类型表达。
掌握基础类型、联合与交叉、条件类型、映射类型和模板字面量类型之后,你就能把业务规则从“注释和口头约定”转化为“编译器可验证的约束”。这正是 TypeScript 在企业级开发中真正的生产力来源。