流畅的 TypeScript - 开篇<全集中·水之呼吸>

196 阅读4分钟

TypeScript 类型流动之美:从实用工具类型说起

breath_of_water

结绳体现了时间的流动,将丝线汇聚在一起编织成型、扭曲、缠绕,有时又还原、断裂,再次连接,那就是时间。 --君の名は。

TypeScript 与 Java 等强类型语言不同,其显著优势在于灵活的类型推断。 这种推断机制不仅可控,而且有别于 Rust 等现代化语言的可选类型注解。 因此,TypeScript 的类型系统具备了流动性,类型不再是静态的定义,而可以像流水一般,被引用、计算、推导和转换。

写在前面

本文既不是 TypeScript 基础指南,也不是 TypeScript 类型挑战 (Type Challenges) 题解。

读者可能需要:

本文旨在从类型流动的角度出发,通过一系列简短的应用场景,讲解一些工具类型的使用场景,旨在帮助读者降低学习 TypeScript 类型的门槛,并体会到如同 JavaScript 般的流畅开发体验。

Be Water, My Friend. --李小龍

be_water

初探:用户 CRUD 类型定义

让我们从一个最常见的场景入手:用户 (User) 信息的增删改查 (CRUD),并为其定义基础类型。

请注意,你可以将鼠标悬停在 console.log 中的变量上,或点击 TypeScript Playground 链接查看类型信息。

// ---cut---
type Resp<T> = any;
// ---cut---
type User = { id: number; name: string; tag: string; };
type List = (query: { id?: number; name?: string; tag?: string; }) => Resp<User[]>;
type Detail = (id?: number) => Resp<User>;
type Create = (neo: { name: string; tag?: string }) => Resp<number>;
type Remove = (id: number) => Resp<boolean>;

const CRUD: { list: List; detail: Detail; create: Create; remove: Remove; } = {
  list: (query) => console.log(query.id, query.name, query.tag),  // 🖱
  detail: (id) => console.log(id), // 🖱
  create: (part) => console.log(part.name, part.tag), // 🖱
  remove: (id) => console.log(id), // 🖱
};

可以看到,即使我们已经尽可能地复用 User 类型来减少类型定义的工作量,{ id, name, tag } 这些字段仍然重复出现多次。有些场景完全相同,有些场景只是将必选属性变为了可选属性。 除了重复编写带来的效率问题,更重要的是,当字段发生变更时(例如将 id 修改为 userId),我们需要手动修改所有相关类型定义,容易出错且维护性差。

接下来,让我们通过使用 TypeScript 的工具类型来进行改进:

- type List = (query: { id?: number; name?: string; tag?: string; }) => Resp<User[]>;
+ type List = (query: Partial<User>) => Resp<User[]>;
- type Detail = (id?: number) => Resp<User>;
+ type Detail = (id?: User["id"]) => Resp<User>;
- type Create = (neo: { name: string; tag?: string; }) => Resp<number>;
+ type Create = (neo: Omit<User, "id">) => Resp<User["id"]>;
- type Remove = (id: number) => Resp<boolean>;
+ type Remove = (id: User["id"]) => Resp<boolean>;

将代码修改如下:

// ---cut---
type Resp<T> = any;
// ---cut---
type User = { id: number; name: string; tag: string; };
type List = (query: Partial<User>) => Resp<User[]>;
type Detail = (id?: User["id"]) => Resp<User>;
type Create = (neo: Omit<User, "id">) => Resp<User["id"]>;
type Remove = (id: User["id"]) => Resp<boolean>;

const CRUD: { list: List; detail: Detail; create: Create; remove: Remove; } = {
  list: (query) => console.log(query.id, query.name, query.tag),  // 🖱
  detail: (id) => console.log(id), // 🖱
  create: (part) => console.log(part.name, part.tag), // 🖱
  remove: (id) => console.log(id), // 🖱
};

可以看到,经过改造后,console.log 中变量的类型信息保持了一致性! 是不是很简单?我们仅仅运用了三个最基础的工具类型,就让类型展现出了流动性

在上述例子中,我们用到了以下几种类型的引用方式:

  • 索引类型访问 (Type<"Index">): 用于获取类型 Type 中指定索引 "Index" 的属性类型。
  • Partial 工具类型 (Partial<Type>): 将类型 Type 的所有属性转换为可选 (Optional) 属性。
  • Omit 工具类型 (Omit<Type, Keys>): 从类型 Type 中排除 Keys 中指定的属性,构建一个新的类型。

除了减少字段的重复编写之外,我们还额外达成了以下目标:

  1. type Create = (neo: Omit<User, "id">) => Resp<User["id"]>; 类型定义更具语义化,清晰地表达出 Create 操作返回的是用户的 id
  2. 联动修改:将 User 类型中 id 属性的类型从 number 修改为 string,只需要修改一处代码即可!
  3. 改动警告:当我们将 User 类型上的 id 属性名称修改为 userId 时,TypeScript 会在所有需要修改的地方提示错误! 如下所示:
// @errors: 2339 2322
// ---cut---
type Resp<T> = any;
// ---cut---
type User = { userId: number; name: string; tag: string; };
type List = (query: Partial<User>) => Resp<User[]>;
type Detail = (id?: User["id"]) => Resp<User>;
type Create = (neo: Omit<User, "id">) => Resp<User["id"]>;
type Remove = (id: User["id"]) => Resp<boolean>;

const CRUD: { list: List; detail: Detail; create: Create; remove: Remove; } = {
  list: (query) => console.log(query.id, query.name, query.tag),  // 🖱
  detail: (id) => console.log(id), // 🖱
  create: (part) => console.log(part.name, part.tag), // 🖱
  remove: (id) => console.log(id), // 🖱
};

总结

通过这个简单的例子,相信你已经初步体会到了 TypeScript 类型流动的魅力及其在实际开发中的应用价值。 它不仅能够减少重复代码,更能提升代码的可维护性和可读性。 接下来,我们将继续结合更多实际案例,深入探讨 TypeScript 中常用的类型特性。

祝编码愉快!🎉