Why Brand Type?
TypeScript Brand
(品牌或烙印)类型在代码中有时扮演着非常重要的角色,其中一个核心的作用就是数据类型的验证。假设我们我们有一个函数,基于email
来查询用户信息,签名如下:
1 | function findUserByEmail(email: string): User | null { |
这个函数的签名非常简单,但是却存在一个非常严重的问题,就是缺少对email
参数值的验证,你可以传入任何字符串。当然在实际的业务场景中,你会对email
进行验证,如必须是合法的mail
格式等。 但是处理这些验证逻辑还是有点复杂的,校验代码重复还是小问题,其他诸如email
格式不正确,你该如何处理?抛出异常?返回null
?还需要明确告知调用方,这个也会增加调用方的调用函数的复杂度。
那么能否有一个更好的方式来处理这个问题呢?答案是肯定的,我们可以使用TypeScript
的Brand
类型来解决这个问题。 首先我们调整一下函数的签名,如下:
1 | function findUserByEmail(email: Email): User | null { |
我们要求email
参数必须是Email
类型,这个Email类型就是要确保email
参数值是合法的email
格式。那么如何定义Email
类型呢?这里我们就需要使用到Brand(烙印)类型,Brand
的声明如下:
1 | declare const brand: unique symbol; |
接下来我们只需要声明一个Email
类型,然后函数再使用该Email
类型作为参数类型,如下:
1 | type Email = Brand<string, 'Email'>; |
这个时候你再调用findByEmail('abc@example.com')
,TypeScript
就会报错:类型不匹配,当然这个也是我们想要的结果。那么如何将字符串转换成Email
类型呢? 这里我们就需要使用到TypeScript
的Type Guards
,通过type predicates
来实现,代码如下:
1 | export function isEmail(email: string): email is Email { |
接下来的工作就简单啦,你只要调用一下isEmail
进行验证,然后再调用findUserByEmail
,如下:
1 | const email = "abc"; |
好多同学觉得这个好像很麻烦啊?其实不然,这里说明一下:
DDD
的Specification Design Pattern
: 如果是使用Domain Driven Design
的话,领域对象数据验证这些都是通过Specification
来实现的,这里你可以将isEmail看成是数据验证逻辑即可。- 验证逻辑复用:isEmail承担数据验证的职责,这样你就可以在其他地方复用这个验证逻辑了,比如在
Controller
层,Service
层等,都没有问题。
通过Brand
的介入,Type Safe
和Data Validation
同时都得到了保障,这是非常好的一件事情。
Zod and Brand Type
Brand Type
一个核心的功能就是数据的验证,当然Zod
在这方面是行家,所以我们结合一下Zod
完成Brand Type
的数据验证逻辑,样例代码如下:
1 | declare const brand: unique symbol; |
这里我们使用Zod
来定义Email
的Schema
,然后通过safeParse
来验证email
是否合法,如果合法则返回success
为true
,否则为false
,这样Type Guard
就可以使用了。
Brand Type的其他场景
上述的列子中,我们使用Brand Type
来验证数据格式的合法性,如果和结合Specification Design Pattern
,还可以用来验证数据的业务逻辑,比如创建用户时要保证email
唯一,我们就可以创建一个UniqueEmail
的Brand Type
, 然后调整一下函数的签名,这样就可以完成业务层的逻辑验证,代码如下:
1 | function createUser(email: UniqueEmail, userInfo: UserInfo): User | null { |
这样我们就可以在创建用户时,先调用isUniqueEmail
来验证email
是否唯一,然后再调用createUser
来创建用户。
这里我们只讨论了基本类型,如果是Object
也是完全没有问题的,而且Object
和Zod
结合也非常方便,这里就不再赘述了。
总结
我们都知道TypeScript
最擅长的事类型,合适的类型定义可以让我们的代码更加安全,而且对开发者理解代码帮助也特别大。 Brand Type
可以保证代码调用的Type Safe
(类型安全),Zod
可以保证数据验证的逻辑(你也可以使用Specification Design Pattern
),两者的结合会让我们代码更安全也更安全。