TypeScript 品牌类型
本文介绍如何在TypeScript中实现品牌类型(也称为不透明类型),以在类型系统层面区分结构相同的类型,解决了TypeScript默认的结构化类型系统所带来的类型兼容性问题。
核心问题
TypeScript采用结构化类型系统,即只要两个类型的结构(属性及其类型)完全相同,它们就是兼容的,可以互换使用。但在某些场景下,我们希望在类型层面上强制区分结构相同的类型,以确保类型安全。
解决方案:品牌化
通过为类型添加一个唯一的品牌标识字段,使其在类型系统中变得不同。关键点在于:
- 使用
Symbol作为品牌字段名:确保该字段不会与类型中的其他业务字段发生冲突。
- 仅在类型层面操作:品牌字段是通过类型运算符添加的,不会影响运行时的数据结构(类型在编译后被擦除)。
- 依赖品牌值进行区分:类型是否兼容不仅取决于结构,还取决于品牌字段的值是否相同。
实现与用例
基本品牌化:通过交叉类型为现有类型添加品牌。
const brand = Symbol('brand'); // 保持私有
type A = { x: number } & { [brand]: 'A' };
type B = { x: number } & { [brand]: 'B' };
此时,类型 A 和 B 虽然结构部分相同,但由于品牌值 'A' 和 'B' 不同,它们不再兼容。
泛型品牌化工具类型:创建可复用的品牌化和反品牌化类型工具。
type Brand<T, U> = T & { [brand]: U };
type RemoveBrand<T> = T[Exclude<keyof T, typeof brand>];
使用 Brand<T, U> 可以轻松地将类型 T 品牌化为 U;使用 RemoveBrand<T> 可以移除类型的品牌。
实际应用(类与构造函数):品牌化不能直接用于类的实例化,需要通过对构造函数进行类型转换来实现。
type Ctor<T> = new (...args: any[]) => T;
const addBrand = <T>(ctor: Ctor<T>, name: string) => {
return ctor as Ctor<Brand<T, typeof name>>;
};
// 用法
class Dog { constructor(public name: string) {} }
const DogBranded = addBrand(Dog, 'Dog');
const dog = new DogBranded('Spot'); // 类型为品牌化的 Dog
对应的,可以通过类型断言移除品牌的附加信息。
总结
品牌类型提供了一种纯粹在TypeScript类型系统中实现名义类型的方法。它通过添加一个具有唯一标识值的编译时字段,强制区分结构相同的类型,从而增强了类型安全性,且不会带来任何运行时开销。文中提供的Brand和RemoveBrand等泛型工具类型使得这一技术易于在项目中应用。