安全与风控
设计类型时如何让非法状态无法表示、避免类型谎言与漏处理分支
在任何静态类型语言中设计类型、审查函数签名或写代码时,用类型系统在编译期消除不可能状态、语义混淆与未处理变体。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
设计类型时如何让非法状态无法表示、避免类通用
解决思路
- 把非法状态做成不可表示:用和类型建模变体(TypeScript 用可辨识联合,Rust/Swift/Kotlin 用带载荷的 enum,Scala 用 sealed class,Haskell/OCaml 用 ADT);不要用一堆可选字段建模状态,否则矛盾组合也能通过编译。反例:`{ completed: boolean; completedAt?: Date }` 允许 `completed: true; completedAt: undefined`,应从单一来源派生布尔值(如 `completedAt !== null`),或显式建模为 `{ kind: 'open' } | { kind: 'done'; at: Date }`。如果某个 bug 让你问“这个组合真的可能发生吗?”,说明类型太松。
- 把类型当作构造而非限制:从想要的值出发构造类型,而不是从更松的类型里用检查裁出来。看似需要 refinement type 的不变量,通常只差一次构造。非空列表是 head 加 rest,不是带长度检查的列表;合法时间区间是起点加时长,不是两个必须保持有序的时间戳。没有哪种表示是特权表示:pair 的列表按某种解释就是偶数长度列表,所以选择无法构造非法值的形状,并在其上暴露调用者需要的接口。
- 给语义化原始类型打品牌:`UserId` 和 `OrderId` 底层都是字符串但不应可互换。Rust 用 newtype,Swift 用 opaque type,Kotlin 用 value class,Haskell 用 phantom type,TypeScript 用 branded intersection。在创建时校验一次,下游信任该类型。
- 外部数据在被解析前都是无类型的:RPC 载荷、JSON、IPC 消息、CLI 参数、配置文件、环境变量、数据库行。在每个边界都要有 parse 函数,把非结构化输入转成类型化模型。校验放在哪里参见 **boundary-discipline** 原则技能。
- 不要对类型系统撒谎:cast、unsafe coercion、绕过编译器的断言函数都是潜在的运行时崩溃。如果编译器无法证明某个事实,要么去证明它(校验、收窄、细化模型),要么承认这个 cast 是危险源。
- 穷尽匹配是编译器的职责:对和类型做匹配# 时,新增变体而未处理必须导致编译失败。使用语言提供的惯用法:TypeScript 用 `never` 类型绑定,Rust 用不带注解的 `match`,Haskell 用 `-Wincomplete-patterns`,Kotlin 用 sealed class 匹配穷尽性。
- 从权威 schema 派生类型:当 protobuf、OpenAPI spec、GraphQL schema、数据库迁移或设计系统 token 文件定义了形状时,从它派生,而不是手写一个平行类型。参见 **encode-lessons-in-structure** 原则技能。
- 只在出现部分性(partiality)的地方强化类型:运行时断言、null 检查、"this should never happen" 抛错,都标记了类型太弱的位置。把该检查上推到类型里,然后停下。类型系统的职责是追踪每个使用点必须处理的情况,而不是尽可能精确地描述数据。优先使用全函数:空列表的 `sum` 是 0,所以它接受普通列表;空列表的 `head` 没有答案,所以它要求非空列表。
- 用以下测试自查:"我能写注释解释这组字段何时有效吗?"能则类型太松,拆成和类型。"两个函数参数共享原始类型但含义不同吗?"打品牌。"这个 `any`、`as`、`assertNotNull` 从哪来?"追溯到边界并在那里校验。"下个月新增变体时,编译器会告诉下一个 agent 在哪里加 case 吗?"不会则匹配不穷尽。"这个类型是否重复了另一个文件拥有的形状?"应派生。"我强化这个类型是为了保持操作全函数,还是只是为了更精确?"如果没有任何东西会因此 panic,就保留普通类型。
适用边界 · 注意事项
- 当类型系统无法表达所需不变量、或语言不支持和类型/品牌类型等构造时,这些模式难以直接套用。
- 当强化类型会显著增加复杂度却没有任何操作会因此 panic 时,不要为了精确而精确。
- 当外部数据边界无法控制或 schema 不稳定、无法可靠派生时,强行从权威 schema 派生可能不现实。