Effect Schema 的 brand 怎么防止 ID 传错?
用户 ID 和订单 ID 同为 string,transfer(from: string, to: string) 的两个参数调换顺序,编译器也不会吭声。
用 Schema.brand 给每种值一个品牌:运行时它们仍是字符串,类型上却是互不兼容的两种东西。 把品牌叠在校验之上,还能把「已验证」记进类型。
这里要先说清 Effect 的 Schema:它同时描述运行时的校验与编译期的类型,Type 是内部形态。brand 就是在这个 Type 上加一个名义标记。整体建模思路见 用 Effect Schema 怎么把领域规则写进类型?。下面讲写法、与校验的结合,以及它保证不了什么。
brand 怎么写?
import { Schema } from "effect"
const UserId = Schema.String.pipe(Schema.brand("UserId"))
const OrderId = Schema.String.pipe(Schema.brand("OrderId"))
type UserId = typeof UserId.Type
type OrderId = typeof OrderId.Type
declare function cancel(order: OrderId): void
// cancel(someUserId) // 编译错误:UserId 不能当作 OrderIdbrand 能叠在校验上吗?
可以,把「通过了校验」这个事实记在类型里:
const Cents = Schema.Int.pipe(Schema.brand("Cents"))
const Email = Schema.String.check(Schema.isPattern(/^[^@]+@[^@]+$/)).pipe(
Schema.brand("Email")
)
const price = Cents.make(1999) // 通过校验才能得到 Cents拿到 Email 类型的值,就等于拿到一份「它已经被验证过」的证据。函数签名写 sendTo(email: Email),就不必在函数里再检查格式。边界校验一次、内部信任类型,brand 就是这份信任的载体。边界解码见 Effect Schema:在边界处解码数据。
brand 在运行时有保证吗?
没有。brand 是编译期的名义区分,运行时它还是原来的字符串或数字。它的可靠性取决于「值只能经由 make 或解码得到」。如果在代码里写 x as Email,就绕过了所有保证。所以要守住一条线:构造品牌值只能走 Schema,不能手写断言。
哪些值值得加 brand?
真正会混淆、混淆代价高的值:各类 ID、金额单位。给每个字符串都加 brand 只会增加噪音。
示例代码取自我整理的教程章节,没有标注 effect 版本号,这次改写中我也没有重新运行;API 写法以所用 effect 版本的文档为准。