Hacker News 中文摘要

RSS订阅

解析而非验证——在一种不希望你这样做的语言中 -- Parse, Don't Validate – In a Language That Doesn't Want You To

文章摘要

本文讨论了编程中“解析而非验证”的原则,强调解析器能将数据转化为更精确的类型并保留信息,而验证器会丢弃信息。作者指出TypeScript代码常犯验证错误,并提倡通过解析获得类型安全,从而减少重复检查,提升代码可靠性和开发效率。

文章总结

好的,这是根据您的要求,对原文主要内容进行的中文重述,保留了核心细节,并删减了与主题无关的评论性内容。


标题:解析,而非验证——在一个不鼓励你这样做语言中

核心观点: 本文探讨了 Alexis King 提出的“解析,而非验证”这一编程原则,并重点分析了在 TypeScript 中实践该原则的挑战与方法。

问题所在: 传统的验证函数(如 isValidUser)在检查完数据后,会丢弃验证结果。例如,验证通过后,user.email 的类型仍然是 string,类型系统“忘记”了它已经是一个合法的邮箱。这导致后续代码需要反复进行防御性检查,作者称之为“散弹枪式解析”。

理想方案: 我们希望创建一个解析函数,它接收原始数据,要么返回一个带有更精确类型(如 ValidUser)的结果,要么返回错误。一旦解析成功,类型本身(如 EmailAge)就成为了数据合法的证明,后续代码无需再检查。

TypeScript 中的实现方法:

  1. 品牌类型(Branded Types): 由于 TypeScript 是结构类型系统,无法像 Haskell 那样直接创建新类型。因此,需要利用“品牌”技巧,通过添加一个仅在类型层面存在的、不导出的 unique symbol 属性,来创建名义上的新类型(如 type Email = string & { readonly [EmailBrand]: true })。这使得 Emailstring 在编译时互不兼容。

  2. 解析函数: 解析函数是唯一被允许使用类型断言(as Email)来“欺骗”类型系统的地方。它返回一个带标签的联合类型(如 Parsed<T>),明确表示成功({ kind: "ok", value: T })或失败({ kind: "err", error: ParseError })。调用者必须处理这两种情况。

  3. 领域模型分离: 将原始数据(UnvalidatedUser)和经过解析的可信数据(ValidUser)定义为不同的类型。所有原始数据在进入系统时,都必须通过解析函数才能转换为可信类型。

TypeScript 的不足之处:

  • 需要类型断言: 在解析函数内部必须使用 as Email 进行断言,这需要严格的纪律来确保断言不会泄露到其他代码中。
  • 穷尽性检查不完善: 虽然可以通过 never 类型实现穷尽性检查,但不如 Elm 等语言那样原生和方便。
  • JSON.parse 返回 any 这是问题的根源之一,建议立即将其结果注解为 unknown

关于 Zod 等库: Zod 等库提供了更便捷的“模式即解析器”的 DSL,能同时生成解析器和类型,是很好的工具。但它们不改变核心的思维方式——开发者仍需主动在边界使用解析,并抵制使用类型断言绕过错误处理的诱惑。

核心原则总结: 让类型系统承载验证的证明,而不是依赖开发者的记忆。每次检查数据后,如果不将结果编码到类型中,就是在要求未来的自己记住这次检查,而未来的自己很可能记不住。在 TypeScript 中,这意味着要依靠品牌类型、带标签的联合类型,并严格区分 unknown(外部输入)和领域类型(可信数据)。

评论总结

根据评论内容,总结如下:

主要观点与论据:

  1. 支持“解析而非验证”理念(评分:正面)

    • 评论1(robertlagrant):“This feels right, and I also have never done it (or had the guts to get others to do it).” 认为理念正确但实践困难,建议将逻辑封装在领域对象或辅助函数中。
    • 评论3(hankbond):“As a new TypeScript user these are concepts that have greatly helped me simplify my code and improve reliability.” 认为该理念简化代码并提升可靠性,推荐使用分离和Linter规则。
  2. 对TypeScript类型系统的质疑(评分:混合)

    • 评论8(rzmmm):“Is there benefit of using this branded type over just encapsulating the raw string in a private variable in closure or class?” 质疑品牌类型是否优于封装,认为可能只是强制名义类型。
    • 评论20(simonreiff):“You get zero runtime type safety guarantees, plus it's often harder to tell in TypeScript whether the transpilation will result in an efficient and performant implementation.” 批评TypeScript缺乏运行时类型安全,且结构类型系统导致意外兼容。
  3. 推荐Zod作为折中方案(评分:正面)

    • 评论7(Altern4tiveAcc):“Zod is by far the most ergonomic way to express those ideas in TypeScript these days.” 认为Zod是表达这些理念最符合人体工程学的方式,但生态系统摩擦真实存在。
    • 评论12(ramon156):“Zod is the acceptable middleground in my opinion.” 认为Zod是可接受的折中方案,适合大多数项目。
  4. 对过度类型抽象的担忧(评分:负面)

    • 评论11(throwaw12):“it feels like it is going to bloat the interface space, how do you tackle this problem?” 担心类型膨胀问题,如为不同字段组合创建过多类型。
    • 评论17(philipwhiuk):“The problem with encoding stuff in type systems is where you stop.” 指出类型系统编码的边界问题。
  5. 其他语言替代方案(评分:中性)

    • 评论10(exceptione):“you can do from F# directly from fable... allows you to program by default in a type safe manner.” 推荐F#通过Fable编译到JavaScript,提供默认类型安全。
    • 评论19(toolslive):“You don't have to use TypeScript if you don't want to: you can compile Haskell, Ocaml, Rust, F#, ... to javascript.” 建议使用其他语言编译到JavaScript。

平衡性总结: - 支持方认为“解析而非验证”能提升代码可靠性,但实践困难。 - 质疑方指出TypeScript类型系统局限性,推荐Zod或JSON Schema作为折中。 - 反对者担心类型膨胀和过度抽象,建议保持简单。 - 部分评论推荐使用其他语言(如F#、Haskell)编译到JavaScript以获得更强类型保证。