TypeScript 类型流动之美:从实用工具类型说起
结绳体现了时间的流动,将丝线汇聚在一起编织成型、扭曲、缠绕,有时又还原、断裂,再次连接,那就是时间。 --君の名は。
TypeScript 与 Java 等强类型语言不同,其显著优势在于灵活的类型推断。 这种推断机制不仅可控,而且有别于 Rust 等现代化语言的可选类型注解。 因此,TypeScript 的类型系统具备了流动性,类型不再是静态的定义,而可以像流水一般,被引用、计算、推导和转换。
写在前面
本文既不是 TypeScript 基础指南,也不是 TypeScript 类型挑战 (Type Challenges) 题解。
读者可能需要:
本文旨在从类型流动的角度出发,通过一系列简短的应用场景,讲解一些工具类型的使用场景,旨在帮助读者降低学习 TypeScript 类型的门槛,并体会到如同 JavaScript 般的流畅开发体验。
Be Water, My Friend. --李小龍
初探:用户 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
中指定的属性,构建一个新的类型。
除了减少字段的重复编写之外,我们还额外达成了以下目标:
type Create = (neo: Omit<User, "id">) => Resp<User["id"]>;
类型定义更具语义化,清晰地表达出Create
操作返回的是用户的id
。- 联动修改:将
User
类型中id
属性的类型从number
修改为string
,只需要修改一处代码即可! - 改动警告:当我们将
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 中常用的类型特性。
祝编码愉快!🎉