类型系统进阶
本章聚焦 TypeScript 近几年引入的高级类型能力,帮助你在真实工程中写出既安全又可维护的类型逻辑。
条件类型与分发机制
条件类型的基本语法是 T extends U ? X : Y
,可用来基于约束做分支推断:
type ID<T> = T extends string | number ? T : never;
const a: ID<string> = '42';
const b: ID<boolean> = true; // error 类型“boolean”不能赋给类型“never”
ts
当条件类型直接作用于联合类型时,会产生分发行为,等价于对联合的每个成员分别运算再合并结果:
type ExcludeNullish<T> = T extends null | undefined ? never : T;
type Cleaned = ExcludeNullish<string | null | undefined>; // string
ts
若不希望分发,可将参数包裹在元组或对象中:[T] extends [U] ? ...
。
infer
解构返回类型
infer
关键字可以在条件类型中声明一个待推断的类型变量,用于拆解函数、Promise 或嵌套结构:
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
type Foo = ReturnTypeOf<() => Promise<number>>; // Promise<number>
ts
结合递归条件类型可实现深度提取:
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
async function load() {
return fetch('/api').then(r => r.json());
}
type ResponseData = Awaited<ReturnType<typeof load>>; // any (取决于 fetch 类型)
ts
模板字面量类型与模式匹配
模板字面量类型让我们可以在类型层面拼接、替换字符串或解析路径:
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
ts
基于模式匹配可以做字符串解析:
type RouteParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? Param | RouteParams<`/${Rest}`>
: Path extends `${string}:${infer Last}`
? Last
: never;
type Params = RouteParams<'/posts/:id/comments/:commentId'>; // 'id' | 'commentId'
ts
映射类型与修饰符控制
映射类型允许对现有类型的每个属性批量变换:
type ReadonlyDeep<T> = {
readonly [K in keyof T]: T[K] extends object ? ReadonlyDeep<T[K]> : T[K];
};
ts
可以通过 +
/ -
修改 readonly
、?
等修饰符:
type Mutable<T> = {
-readonly [K in keyof T]-?: T[K];
};
ts
keyof
+ 索引访问符 (T[K]
) 是构建映射类型的基础,前者获得属性联合,后者复用原有值类型。
常用内置工具类型
TypeScript 已内置了大量基于条件/映射类型实现的工具,理解它们有助于编写自定义类型。
Partial<T>
:将所有属性设为可选。Required<T>
:与Partial
相反,移除?
。Readonly<T>
/Mutable<T>
:控制readonly
修饰符。Pick<T, K>
/Omit<T, K>
:按键名选择或排除属性。Record<K, V>
:构造键为K
、值为V
的对象类型。NonNullable<T>
:移除null | undefined
。Extract<T, U>
/Exclude<T, U>
:取交集或差集。ReturnType<T>
/Parameters<T>
:从函数签名中提取返回值或参数元组。InstanceType<T>
:获取类构造函数的实例类型。Awaited<T>
:递归剥离 Promise。
如果需要在项目组内共享自定义工具类型,建议集中存放于
types/utility.ts
,并配合单元测试验证关键推断逻辑(可借助tsd
或expectTypeOf
)。
变型(Variance)与函数参数检查
TypeScript 默认允许函数参数双向协变(方便但不够安全)。在开启 strictFunctionTypes
后,函数参数会变得更严格:
let acceptsNumber = (n: number) => {};
let acceptsNumberOrString = (n: number | string) => {};
acceptsNumber = acceptsNumberOrString; // error (strictFunctionTypes = true)
ts
理解变型有助于编写类型安全的泛型结构,例如只读集合类型应通过 readonly
限定内部数据,避免误用导致类型倒流。
模式与实践建议
- 组合已有工具类型比手写
extends
链更易读,遇到复杂逻辑时先拆成多个中间类型。 - 尽量在类型定义中保留字面量信息,如使用
as const
、satisfies
让推断更精确。 - 对外导出的复杂类型可同时配备一个示例对象或函数,降低认知成本。
- 使用 vscode TypeScript 工具操作(
Go to Definition
、Quick Info
)验证推断结果,必要时借助type Foo =
临时 alias 调试。
下一节我们将把这些类型技巧落地到工程实践与团队协作中。
↑