很多人对 TypeScript 的印象停留在"给 JavaScript 加类型标注"这个层面。但当你真正深入它的类型系统,会发现这里有一片令人着迷的天地——类型体操。
这不是炫技,而是让代码更安全、更优雅的实用技能。
为什么需要"类型体操"?
想象这样一个场景:你有一个函数,接收一个对象,返回这个对象中所有函数类型的属性名。如果用 JavaScript 写,你只能依赖运行时检查,可能会漏掉某些边界情况。但在 TypeScript 中,你可以用类型推导,让编译器帮你确保正确性。
基础:条件类型与 infer
条件类型是类型体操的基础语法,类似于三元表达式:
type IsString<T> = T extends string ? true : false;
// 使用示例
type A = IsString<string>; // true
type B = IsString<number>; // false
而 `infer` 关键字则让我们能够在条件类型中"推断"出某个类型:
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
// Promise<number> 会推断出 number
type Result = UnpackPromise<Promise<number>>; // number
这个技巧特别有用。比如,当你处理 API 返回值时,很多时候返回的是 Promise,但你真正需要的是 Promise 内部的类型。
进阶:模板字面量类型
TypeScript 4.1 引入了模板字面量类型,这让字符串类型的处理变得非常强大:
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type HoverEvent = EventName<'hover'>; // 'onHover'
这意味着,你可以根据一个字符串类型,自动生成另一个相关的字符串类型。这种能力在定义事件处理器时特别有用——你不需要手动写 `onClick`、`onHover` 等类型,它们可以自动推导。
实战:提取对象中的函数属性名
让我们来实现一个实用的类型:从一个对象类型中,提取所有函数属性的名字。
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
// 使用示例
interface User {
name: string;
age: number;
greet: () => void;
update: (data: Partial<User>) => void;
}
type UserFunctions = FunctionPropertyNames<User>; // 'greet' | 'update'
这个类型的原理是:先遍历对象的所有属性,如果属性值是函数类型,就保留属性名;否则标记为 `never`。最后,通过索引访问 `keyof T`,所有 `never` 会自动被排除,只保留符合条件的属性名。
写类型体操的建议
1. **从简单开始**:不要一开始就写复杂的递归类型,先掌握 `extends`、`infer` 这些基础概念。
2. **写测试用例**:类型体操容易出错,写几个测试用例验证你的类型是否正确工作。
3. **阅读开源项目**:很多优秀的开源项目(如 Vue、React)都大量使用了高级类型技巧,阅读它们的类型定义是很好的学习方式。
4. **不要过度使用**:类型体操很强大,但不是所有场景都需要。如果一个简单的类型标注就能解决问题,就不要用复杂的条件类型。
结语
TypeScript 的类型系统是一门值得深入学习的艺术。当你真正掌握了类型体操,你会发现:代码变得更安全了,错误在编译时就被捕获了,重构时更有信心了。
更重要的是,类型不再是一种"约束",而是一种"表达"——你可以用类型来描述你的设计意图,让代码自己说明自己。
这才是 TypeScript 的真正魅力。