principle-type-system-discipline

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Type System Discipline

类型系统规范

The type checker is a proof assistant. Use it to eliminate impossible states, mismatched primitives, and unhandled variants at compile time. Anything you let through as runtime data becomes a runtime failure the compiler could have stopped.
Applies to any typed language. Skills like
typescript-best-practices
ground it in specific syntax.
The patterns:
  • Make illegal states unrepresentable. Model variants as sum types: discriminated unions in TypeScript, enums with payloads in Rust/Swift/Kotlin, sealed classes in Scala, ADTs in Haskell/OCaml. Don't model state as a bag of optional fields where contradictory combinations compile. A subtle anti-pattern worth naming:
    { completed: boolean; completedAt?: Date }
    admits
    completed: true; completedAt: undefined
    , which is meaningless. Derive the boolean from a single source like
    completedAt !== null
    , or model the variants explicitly as
    { kind: 'open' } | { kind: 'done'; at: Date }
    . If a bug forces the question "wait, can this combination actually happen?", the type is too loose.
  • Brand semantic primitives.
    UserId
    and
    OrderId
    are strings underneath but should not be interchangeable. Newtypes in Rust, opaque types in Swift, value classes in Kotlin, phantom types in Haskell, branded intersections in TypeScript. Validate once at creation, trust the type downstream.
  • External data is untyped until parsed. RPC payloads, JSON, IPC messages, CLI args, config files, environment variables, database rows. Have a parse function at every boundary that turns unstructured input into the typed model. See the boundary-discipline principle skill for where to put validation.
  • Don't lie to the type system. Casts, unsafe coercions, and assertion functions that bypass the compiler are runtime crashes waiting to happen. If the compiler can't prove a fact, prove it (validate, narrow, refine the model) or accept that the cast is a hazard. The cast you bury today is the postmortem you write next week.
  • Exhaustive matching is the compiler's job. When you match on a sum type, the compiler must fail compilation if a new variant is added without handling. Use the idiom your language provides:
    never
    -typed binding in TypeScript, unannotated
    match
    in Rust,
    -Wincomplete-patterns
    in Haskell, sealed-class match exhaustiveness in Kotlin.
  • Derive types from authoritative schemas. When a protocol buffer, OpenAPI spec, GraphQL schema, database migration, or design-system token file defines a shape, derive from it instead of hand-rolling a parallel type. Manual duplication drifts. See the encode-lessons-in-structure principle skill.
  • Prefer compile-time over runtime. Every runtime assertion, null check, and
    instanceof
    is admitting the type system isn't carrying its weight. Push the check up to the type.
The tests:
  • "Can I write a comment explaining when this combination of fields is valid?" If yes, the type is too loose. Split it into a sum type.
  • "Do two of my function arguments share a primitive type but mean different things?" Brand them.
  • "Where did this
    any
    , this
    as
    , this
    assertNotNull
    come from?" Trace it to the boundary and validate there instead.
  • "If a new variant is added next month, will the compiler tell the next agent where to add a case?" If no, the match isn't exhaustive.
  • "Is this type duplicating a shape another file owns?" Derive instead.
类型检查器是一个证明助手。利用它在编译时消除不可能的状态、不匹配的基元和未处理的变体。任何被你放行到运行时的数据,都会变成编译器本可以阻止的运行时错误。
适用于任何类型化语言。诸如
typescript-best-practices
之类的技能将其落地到具体语法中。
模式:
  • 让非法状态无法被表示。 将变体建模为sum types:TypeScript中的discriminated unions,Rust/Swift/Kotlin中带负载的枚举,Scala中的密封类,Haskell/OCaml中的ADTs。不要将状态建模为一组可选字段的集合,因为矛盾的组合也能通过编译。这里要指出一个微妙的反模式:
    { completed: boolean; completedAt?: Date }
    允许
    completed: true; completedAt: undefined
    这种无意义的情况。可以从单一来源(如
    completedAt !== null
    )推导布尔值,或者将变体显式建模为
    { kind: 'open' } | { kind: 'done'; at: Date }
    。如果某个bug让你产生“等等,这种组合真的可能发生吗?”的疑问,说明类型定义过于宽松。
  • 为语义基元添加标识。
    UserId
    OrderId
    本质上都是字符串,但不应被互换使用。可以使用Rust中的Newtypes、Swift中的opaque types、Kotlin中的value classes、Haskell中的phantom types、TypeScript中的branded intersections。在创建时验证一次,之后就可以信任该类型。
  • 外部数据在解析前是无类型的。 RPC payload、JSON、IPC消息、CLI参数、配置文件、环境变量、数据库行。在每个边界处都要有一个解析函数,将非结构化输入转换为类型化模型。有关验证位置的信息,请参阅boundary-discipline原则技能。
  • 绝不欺骗类型系统。 类型转换、不安全强制转换和绕过编译器的断言函数,随时可能导致运行时崩溃。如果编译器无法证明某个事实,要么自行证明(验证、缩小范围、优化模型),要么接受该转换存在风险。你今天埋下的类型转换,就是你下周要写的事后分析报告的原因。
  • 穷尽匹配是编译器的职责。 当你匹配sum type时,如果添加了新的变体却未处理,编译器必须终止编译。使用语言提供的惯用写法:TypeScript中的
    never
    类型绑定,Rust中的无注解
    match
    ,Haskell中的
    -Wincomplete-patterns
    ,Kotlin中的密封类匹配穷尽性检查。
  • 从权威模式派生类型。 当Protocol Buffer、OpenAPI规范、GraphQL schema、数据库迁移或设计系统令牌文件定义了某种结构时,应从中派生类型,而非手动编写并行类型。手动复制会导致差异。请参阅encode-lessons-in-structure原则技能。
  • 优先选择编译时而非运行时。 每一个运行时断言、空值检查和
    instanceof
    操作,都表明类型系统没有发挥应有的作用。将检查向上推至类型层面。
测试方法:
  • “我能否写一段注释来解释这些字段组合何时有效?”如果可以,说明类型定义过于宽松,应将其拆分为sum type。
  • “我的两个函数参数是否共享基元类型但含义不同?”为它们添加标识。
  • “这个
    any
    as
    assertNotNull
    来自哪里?”追踪到边界处,改为在那里进行验证。
  • “如果下个月添加新的变体,编译器是否会提示后续开发人员需要添加处理分支?”如果不能,说明匹配并非穷尽。
  • “这个类型是否复制了另一个文件定义的结构?”改为派生类型。