高级类型编程
概述
TypeScript 的高级类型编程是一套图灵完备的类型级计算系统。条件类型提供分支逻辑,映射类型提供遍历能力,模板字面量类型处理字符串级运算,infer 实现模式匹配。掌握这些工具,可以构建精确的类型约束,将更多错误从运行时提前到编译时。
面试定位:高级类型题是区分中级与高级 TypeScript 开发者的关键。面试官通过 type-challenges 风格的问题,考察候选人对类型系统的深度理解和类型级编程能力。
条件类型(Conditional Types)
基础语法
条件类型的语法类似三元表达式,在类型层面执行条件判断:
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
分布式条件类型(Distributive Conditional Types)
当条件类型作用于裸类型参数(naked type parameter)的联合类型时,会自动分布到每个联合成员上:
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[](而非 (string | number)[])
要阻止分布行为,用方括号包裹类型参数:
type ToArray<T> = [T] extends [unknown] ? T[] : never;
type Result = ToArray<string | number>;
// (string | number)[]
条件类型与联合类型过滤
// 提取函数类型
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];
interface API {
name: string;
getUser: (id: string) => User;
updateUser: (user: User) => void;
}
type Methods = FunctionKeys<API>; // "getUser" | "updateUser"
infer 关键字
infer 在条件类型中引入待推断的类型变量,实现类型级的模式匹配:
基础用法
// 推断函数返回类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// 推断数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never;
type Item = ElementType<string[]>; // string
// 推断 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
type Result = UnwrapPromise<Promise<Promise<string>>>; // string
多位置 infer
当 infer 出现在多个位置时,TypeScript 会尝试统一推断:
// 推断第一个和最后一个元素
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type F = First<[1, 2, 3]>; // 1
type L = Last<[1, 2, 3]>; // 3
infer 与约束(TypeScript 4.7+)
// infer 位置添加 extends 约束
type FirstString<T> = T extends [infer S extends string, ...any[]] ? S : never;
type A = FirstString<["hello", 42]>; // "hello"
type B = FirstString<[42, "hello"]>; // never
映射类型(Mapped Types)
基础语法
映射类型遍历联合类型的每个成员,生成新的属性:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
键重映射(Key Remapping,TypeScript 4.1+)
as 子句允许在映射过程中转换键:
// 为每个属性生成 getter
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
过滤键
通过 as 子句中返回 never 来过滤属性:
// 只保留字符串值的属性
type StringProps<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
}
type OnlyStrings = StringProps<Mixed>;
// { name: string; email: string }
修饰符操作
+ 和 - 前缀可以添加或移除 readonly 和 ? 修饰符:
// 移除 readonly
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// 移除可选
type Required<T> = {
[P in keyof T]-?: T[P];
};
模板字面量类型(Template Literal Types)
TypeScript 4.1+ 引入了类型级的模板字符串:
基础用法
type EventName = `on${Capitalize<"click" | "focus" | "blur">}`;
// "onClick" | "onFocus" | "onBlur"
type Locale = "en" | "zh" | "ja";
type Currency = "USD" | "CNY" | "JPY";
type LocalizedPrice = `${Locale}_${Currency}`;
// "en_USD" | "en_CNY" | "en_JPY" | "zh_USD" | ... (9种组合)
模式匹配与 infer
// 解析路由参数
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"
实际应用:类型安全的 CSS 属性
type CSSValue = `${number}${"px" | "em" | "rem" | "%"}`;
function setWidth(el: HTMLElement, width: CSSValue) {
el.style.width = width;
}
setWidth(el, "100px"); // ✅
setWidth(el, "2em"); // ✅
setWidth(el, "100"); // ❌ 缺少单位
递归类型
TypeScript 4.1+ 对递归类型有良好支持:
// 深层 Readonly
type DeepReadonly<T> = T extends (...args: any[]) => any
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// JSON 类型
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
// 路径类型
type Path<T, K extends keyof T = keyof T> =
K extends string
? T[K] extends object
? K | `${K}.${Path<T[K]>}`
: K
: never;
interface Config {
db: { host: string; port: number };
app: { name: string };
}
type ConfigPath = Path<Config>;
// "db" | "app" | "db.host" | "db.port" | "app.name"
注意:递归深度有限制(通常约 50 层)。过深的递归会触发 Type instantiation is excessively deep and possibly infinite 错误。
实战:类型体操常见模式
元组转联合
type TupleToUnion<T extends readonly any[]> = T[number];
type Union = TupleToUnion<[1, 2, 3]>; // 1 | 2 | 3
联合转交叉
type UnionToIntersection<U> =
(U extends unknown ? (k: U) => void : never) extends
(k: infer I) => void ? I : never;
type Result = UnionToIntersection<{ a: 1 } | { b: 2 }>;
// { a: 1 } & { b: 2 }
获取对象深层值类型
type Get<T, K extends string> =
K extends `${infer First}.${infer Rest}`
? First extends keyof T
? Get<T[First], Rest>
: never
: K extends keyof T
? T[K]
: never;
type Value = Get<{ a: { b: { c: number } } }, "a.b.c">; // number
类型编程的工程边界
高级类型编程强大但有代价:
- 编译性能:复杂的条件类型和递归类型显著增加编译时间
- 可读性:过度复杂的类型让团队成员难以理解和维护
- 错误信息:深层泛型的类型错误信息往往冗长而难以定位
实用原则:
- 能用简单类型表达的,不使用复杂类型编程
- 类型工具应有清晰的文档和测试用例
- 递归深度控制在合理范围内
- 考虑使用
// @ts-expect-error或类型断言作为"逃生舱"
面试高频问题
Q: 什么是分布式条件类型?如何控制其行为?
回答要点:当条件类型的被检查类型是裸类型参数(直接使用 T 而非 [T]),且传入联合类型时,条件判断会分布到每个联合成员上独立执行,最终结果合并为联合类型。用方括号 [T] extends [U] 包裹可以阻止分布行为。典型应用是 Exclude<T, U> 和 Extract<T, U> 的实现。
Q: 请解释 infer 关键字的作用
回答要点:infer 在条件类型的 extends 子句中声明一个待推断的类型变量,让 TypeScript 从匹配的结构中提取类型。它类似于正则表达式中的捕获组——先描述结构模式,再从中提取需要的部分。ReturnType<T> 和 Parameters<T> 等内置工具类型的核心就是 infer。