解析,而不是验证 – 在一个不想让你这样做的语言中
更新:如果你喜欢这篇文章,后续文章《没有效果的效果-TS:简单 TypeScript 中的代数思维》将在我们所提到的基础上进一步讨论这些想法。我一直在思考 Alexis King 的“解析,而不是验证”。我其实经常这样做,通常是在盯着一个 TypeScript 代码库时,它悄悄堆积了许多像藤壶一样的 if (user.email) 检查。这篇文章是2019年的,建议(或者更确切地说,原则)更早之前就存在了。然而,我阅读的大多数 TypeScript 代码——包括我自己写的很多——仍然是验证而不是解析。如果你还没读过这篇文章,建议你去了解一下:验证器会说“这个东西没问题,请继续。”而解析器会说“给我一个 blob,我要么给你一个更精确的类型,要么告诉你我为什么无法做到。”这个区别听起来像是学术性的,直到你意识到,验证器在运行完成的那一刻就丢弃了信息,而解析器通过将其编码到类型中保留了它们所学到的内容。一旦你将字符串解析为 EmailAddress,程序的其余部分就不必再担心了。安心以及更多的思维能力用于有趣的事情。在 Haskell 或 Elm 或 F# 中,这就是写代码的方式。语言会引导你向这种方式靠拢。在 TypeScript 中……它不会。TypeScript 会乐意让你做正确的事情,但它不会坚持,也不会轻轻推动。如果有的话,结构类型会积极破坏整个游戏。让我给你展示我的意思。我们所有人都写过的验证器 这是我常见到(和写到)的代码: interface User { id: number; email: string; age: number; } // 实际的验证是幼稚且简单的,但你明白我的意思: function isValidUser(user: User): boolean { if (!user.email.includes(“@”)) return false; if (user.age < 0 || user.age > 150) return false; return true; } function sendWelcome(user: User) { if (!isValidUser(user)) { throw new Error(“无效用户”); } // …稍后,在调用栈更深层: emailService.send(user.email, `欢迎,年龄 ${user.age} `); } 发现谎言了吗? User.email 只是字符串。 User.age 只是数字。验证完成了——恭喜——但类型系统在 isValidUser 返回的那一瞬间就忘记了它。三次函数调用后,当有人触碰 user.email 时,没有什么能阻止他们将其传递给期望真实邮箱的函数。因为在 TypeScript 看来,它只是字符串。与“”相同,与“hello”相同,与“绝对不是邮箱”相同。那么我们该怎么做?我们重新验证。我们又加了一个 if。我们写了单元测试。我们希望。(King 在原文中为此有一个更好的词:“散弹枪解析”——验证分散在各处,且没有一个被记住。) 我们实际上想要的 我们想要这样的: function sendWelcome(user: ValidUser) { emailService.send(user.email, `欢迎,年龄 ${user.age}`); } 而且我们希望不可能将未经过解析器的东西传递给 sendWelcome。没有重新检查或“防御性编程”。类型本身就像是证明一样。在 Elm 中,我会使用不透明类型和智能构造函数,完成大约四行。在 TypeScript 中,这,嗯,至少是可能的。只是没那么愉快。 品牌类型,或者:故意对结构类型系统撒谎 TypeScript 是结构类型的,这意味着两个具有相同形状的类型是相同的类型。 string 是 string 是 string。没有 newtype。没有 type EmailAddress = String 能够生成真正不同的类型,就像 Haskell 做的那样。社区解决的变通方法是品牌化——也称为标签,也称为通过交集实现的命名类型。廉价版是字符串字面量幻影({ readonly __brand: “Email” }),你会到处看到它;稍微贵一点的版本使用一个你不从模块中导出的独特符号,因此外部的人连拼写品牌的能力都没有: declare const EmailBrand: unique symbol; declare const AgeBrand: unique symbol; type Email = string & { readonly [EmailBrand]: true }; type Age = number & { readonly [AgeBrand]: true }; 在运行时没有品牌字段。这是一个“幻影”——一种类型级标记,使 Email 和字符串在编译时不兼容。获得 Email 的唯一方法是通过一个知道如何做的函数,因为这个模块外没有人能够命名该符号来伪造一个。(TS5 还让你与模板字面量类型调情——type Email = `${string}@${string}`——这对演示来说很有趣,但单靠它不够。)这是让你在不离开语言的情况下使非法状态不可表示的举动。顺便说一下,品牌是单向的:一个 Email 仍然可以赋值给 string。名义进入领域,结构走出去,基本上正是你想要的。那个函数就是你的解析器: type ParseError = { kind: “ParseError”; message: string }; type Parsed<T> = { kind: “ok”; valu
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡