TypeScript 类型系统深度实践:从工具类型到类型层面的编程
TypeScript 的类型系统能力远超”给 JavaScript 加类型注解”。它是一个图灵完备的类型层面的编程语言。理解这一点,才能正确评估何时应该深入使用类型系统,何时应该适可而止。
条件类型:类型层面的 if/else
条件类型允许根据类型关系做出类型层面的决策:
// 提取 Promise 的内部类型
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>; // string
type B = Unwrap<number>; // number
// 实际应用:提取 API 响应类型
type ApiResponse<T> = T extends { data: infer D } ? D : never;
infer 关键字是条件类型的核心——它在类型层面做模式匹配,从复杂类型中提取出子类型。
模板字面量类型:字符串层面的类型运算
TypeScript 4.1+ 引入了模板字面量类型,可以在类型层面操作字符串:
// 为事件系统生成类型安全的处理函数名
type EventName = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'
// 实际应用:类型安全的路由参数提取
type Route = `/users/${string}` | `/posts/${string}/comments`;
type ExtractParam<T extends string> =
T extends `${infer _Start}/${infer Param}/${infer _Rest}`
? Param
: never;
类型守卫的实际模式
is 类型守卫不仅用于基础类型判断,在数据验证层非常有用:
// 运行时验证 + 编译时类型收窄
interface User {
id: number;
name: string;
email: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value && typeof (value as User).id === 'number' &&
'name' in value && typeof (value as User).name === 'string' &&
'email' in value && typeof (value as User).email === 'string'
);
}
// 使用:从 API 获取数据后验证
const data: unknown = await fetch('/api/user').then(r => r.json());
if (isUser(data)) {
// data 的类型在这里被收窄为 User
console.log(data.email.toUpperCase());
}
这种模式在处理外部数据(API 响应、localStorage、URL 参数)时特别有用,因为它同时提供了运行时安全和编译时类型安全。
何时不要深入类型体操
类型系统的一个陷阱是过度工程。判断标准:
- 如果类型定义比实现代码还长,重新考虑设计。一个 50 行的泛型工具类型通常意味着你把运行时逻辑搬到了类型层面,这不是 TypeScript 的设计初衷
- 不要为了”完美类型”牺牲可读性。一个
Record<string, unknown>配合运行时的类型守卫,比一个 30 行的递归条件类型更容易维护 as断言不是罪恶。当你比 TypeScript 更了解类型时(比如从 JSON 解析后手动构造了对象),使用as是合理的。关键是确保断言前后有足够的运行时保证
工具类型的实际使用频率
在真实项目中,你 90% 的时间只需要这 5 个工具类型:
Partial<T>:表单草稿、配置选项Pick<T, K>:API 响应中只暴露部分字段Omit<T, K>:从类型中排除敏感字段(如 password)Record<K, V>:键值映射,如路由表ReturnType<T>:从函数类型中提取返回值类型
TypeScript 的价值不是写出”类型体操冠军”级别的泛型,而是用最少的类型代码捕获最多的运行时错误。保持简单,按需使用。