1. 巧用 keyof

假如我们要实现一个 getValue 函数,作用是根据传入的 object 和 key 获取 object 上 key 所对应的属性值 value,一开始的 JavaScript 版本可能是这样的。

  1. const data = {
  2. a: 3,
  3. hello: 'world'
  4. }
  5. function getValue(o, name) {
  6. return o[name]
  7. }

可以看到这就是 JavaScript 的弊端,我们对传入的 o 和 name 都没有约束,对这一切都是未知的,人的恐惧就源自对未知事物的不确定性。尤其如果这是一个底层类库,我们不知道有多少人去调用,那这个风险就更加增大了,所以我们可能用 TypeScript 这样去改造它。

  1. function getValue(o: any, name: string) {
  2. return o[name]
  3. }

但这样写貌似也有很多问题,函数返回值的类型变成了 any,失去了 TypeScript 类型校验的功能,让类型重新变的不可控,name 类型固然是 string,但还是太宽泛了,实际上,name 的值只能是o的键的并集,而且如果我们将 name 拼写错误,TypeScript 也不会帮我们报错提示。这个时候我们可以使用 keyof 结合泛型来加强 getValue 函数的类型功能。

  1. function getValue<T extends object, K extends keyof T>(o: T, name: K): T[K] {
  2. return o[name]
  3. }

2. 接口智能提示

  1. interface Seal {
  2. name: string;
  3. url: string;
  4. }
  5. interface API {
  6. "/user": { name: string; age: number; phone: string };
  7. "/seals": { seal: Seal[] };
  8. }
  9. const api = <URL extends keyof API>(url: URL): Promise<API[URL]> => {
  10. return fetch(url).then((res) => res.json());
  11. };

借助泛型以及泛型约束,我们可以实现智能提示的功能,不光接口名可以智能提示,接口返回也可以智能提示。当我们输入 api 的时候,其会自动将 API interface 下的所有 key 提示给我们,当我们输入某一个 key 的时候,其会根据 key 命中的 interface 定义的类型,然后给予类型提示。这在统一接口管理方面有很大的用处,可以帮助我们面向接口编程。
TypeScript 小技巧 - 图1
TypeScript 小技巧 - 图2
TypeScript 小技巧 - 图3

3. 巧用类型保护

  1. interface User {
  2. name: string;
  3. age: number;
  4. occupation: string;
  5. }
  6. interface Admin {
  7. name: string;
  8. age: number;
  9. role: string;
  10. }
  11. export type Person = User | Admin;
  12. export const persons: Person[] = [
  13. {
  14. name: 'Max Mustermann',
  15. age: 25,
  16. occupation: 'Chimney sweep'
  17. },
  18. {
  19. name: 'Jane Doe',
  20. age: 32,
  21. role: 'Administrator'
  22. },
  23. {
  24. name: 'Kate Müller',
  25. age: 23,
  26. occupation: 'Astronaut'
  27. },
  28. {
  29. name: 'Bruce Willis',
  30. age: 64,
  31. role: 'World saver'
  32. }
  33. ];
  34. export function logPerson(person: Person) {
  35. let additionalInformation: string;
  36. if (person.role) {
  37. additionalInformation = person.role;
  38. } else {
  39. additionalInformation = person.occupation;
  40. }
  41. console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
  42. }
  43. persons.forEach(logPerson);

TypeScript 小技巧 - 图4
我们可以看到,当我们定义了两种 Person 类型:User 和 Admin,而在使用的时候是比较宽泛的 Person,那我们就不能直接使用 User 或者 Admin 的特有属性 role 或者 occupation。因为 TypeScript 没有足够的信息确定 Person 究竟是 User 还是 Admin。
一种方法是使用类型断言,显示的告诉 TypeScript,person 就是 Admin 类型或者就是 User 类型,但是这样做一方面不够优雅,要在每一处都加上断言;另一方面滥用断言也会让我们的代码变得不可控,不能让 TypeScript 帮助我们进行合理的类型推断。像双重断言可以规避掉 TypeScript 的类型检查机制也是与 any 一样,要尽可能去避免的。
TypeScript 小技巧 - 图5
正确的做法是使用类型收缩,比如使用 is,in,typeof,instanceof 等,使得 TypeScript 能够 get 到当前的类型,假如 person 上面有 role 属性,TypeScript 就可以推断出 person 就是 Admin 类型,创建类型保护区块,在当前的代码块按照 Admin 类型处理,代码也简洁了很多。
TypeScript 小技巧 - 图6
同样是这个例子,我们再改造一下,通过两个函数来判断 person 的具体类型是 Admin 还是 User。但是很不幸,TypeScript 依然不能很智能的知道 person 在第一个代码块里是 Admin 类型,在第二个代码块里是 User 类型。
TypeScript 小技巧 - 图7
这个时候我们就要改造一下 isAdmin 和 isUser 的函数返回,创建用户自定义的类型保护函数,显式的告诉 TypeScript,函数返回为 true 时,指定 person 的类型确定为 Admin 或者 User,这样 TypeScript 就知道 person 的确定类型了,这就是类型位词。
TypeScript 小技巧 - 图8

4.常用的高级类型

这其实涉及到了类型编程到概念,简而言之,我们平时写代码是对值进行编程,而类型编程是对类型进行编程,可以利用 keyof 对属性做一些扩展,省的我们要重新定义一下接口,造成很多冗余代码。
这些高级类型在日常编程中有非常广泛的使用,尤其 Partial 可以将所有的属性变成可选的,如在我们日常的搜索逻辑,我们可以根据单一条件搜索,也可以根据组合条件搜索。Omit 可以帮助我们复用一个类型,但是又不需要此类型内的全部属性,当父组件通过 props 向下传递数据的时候,可以剔除一些无用的类型。
Record 也是一个比较常用的高级类型,可以帮助我们从 Union 类型中创建新类型,Union 类型中的值用作新类型的属性。当我们拼写错误,或者漏写一些属性,或者加入了没有预先定义的属性进去,TypeScript 都可以给我们很友好的报错提示。

  1. type Partial<T> = {
  2. [P in keyof T]?: T[P];
  3. };
  4. type Required<T> = {
  5. [P in keyof T]-?: T[P];
  6. };
  7. type Pick<T, K extends keyof T> = {
  8. [P in K]: T[P];
  9. };
  10. type Exclude<T, U> = T extends U ? never : T;
  11. type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'>
  12. type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
  13. type Record<K extends keyof any, T> = {
  14. [P in K]: T;
  15. };
  16. interface User {
  17. id: number;
  18. age: number;
  19. name: string;
  20. };
  21. type PartialUser = Partial<User>
  22. type PickUser = Pick<User, "id" | "age">
  23. type OmitUser = Omit<User, "id">
  24. type AnimalType = 'cat' | 'dog' | 'frog';
  25. interface AnimalDescription { name: string, icon: string }
  26. const AnimalMap: Record<AnimalType, AnimalDescription> = {
  27. cat: { name: '猫', icon: ' '},
  28. dog: { name: '狗', icon: ' ' },
  29. forg: { name: '蛙', icon: ' ' },
  30. };

5.巧用类型约束

在 .tsx 文件中,泛型可能会被当作 jsx 标签
const toArray = <T>(element: T) => [element];
加 extends 可破
const toArray = <T extends {}>(element: T) => [element];
TypeScript 还可以给一些缺乏类型定义的第三方库定义类型,找到一些没有 d.ts 声明的开源库,为开源社区贡献声明文件。

学习参考

  1. https://www.typescriptlang.org/docs/handbook/release-notes/overview.html 官方各个版本文档
  2. https://github.com/microsoft/TypeScript/projects/9 官方讨论
  3. https://github.com/microsoft/vscode VS Code 是 TypeScript 编写的,毫无疑问也是学习的好地方
  4. https://basarat.gitbook.io/typescript/getting-started TypeScript Deep Dive
  5. https://github.com/typescript-exercises/typescript-exercises TypeScript 优秀的练习题