pstack原则09类型系统纪律:如何让非法状态不可表示
【免费下载链接】pstack-claudeClaude Code, Codex, Copilot, Pi, OpenCode, Gemini, and Prime Agent versions of Poteto's pstack. Rigorous agent workflows with Cursor primitives translated for other harnesses.项目地址: https://gitcode.com/GitHub_Trending/ps/pstack-claude
pstack 类型系统纪律是 pstack 原则系列的第 09 条:把类型检查器当作"证明助手",在编译期就消灭非法状态。读完本文,你会掌握判别联合、品牌类型、边界解析等 8 个实操模式,学会让非法状态不可表示——从源头减少运行时错误,而不是出错后再打补丁。
为什么需要"类型系统纪律"?
类型检查器不只是报错工具,它是一位证明助手(proof assistant)。核心思想一句话:
类型系统让你忽略掉的每一种情况,都会变成编译器本可以阻止的运行时崩溃。
比起到处添加防御性检查,更好的做法是:把错误和特例直接定义到不存在。这条原则适用于任何静态类型语言(TypeScript、Rust、Kotlin、Swift……),在 pstack 仓库中写作技能文件 principle-type-system-discipline/SKILL.md。
判别联合:让矛盾状态根本写不出来
新手最容易掉进"可选字段袋"陷阱:
// 坏味道:completed 为 true 却没有时间,编译器却不报错 type Task = { completed: boolean; completedAt?: Date };这个定义允许"已完成但没有完成时间"这种无意义的状态。用判别联合(discriminated union)重写即可:
// 好:每个合法状态都是独立分支,非法组合无法构造 type Task = { kind: 'open' } | { kind: 'done'; at: Date };每个变体用kind字面量区分,"已完成但没有时间"从此无法被写出。这就是"让非法状态不可表示"的精髓。
类型是"构造"出来的,不是"限制"出来的
别拿运行时检查去雕刻一个宽松类型,而是从合法值出发把类型搭起来:
- 非空列表 = 首元素 + 其余元素,而不是"列表 + 到处检查长度"
- 有效时间区间 = 开始时间 + 时长,而不是两个必须手动保序的时间戳
原文有个很妙的判据:当某个 bug 让你问出"等等,这种组合真的可能发生吗?"时,说明类型太松了。
品牌类型:UserId 和 OrderId 不能互换
UserId和OrderId底层都是字符串,但绝不该互换。用品牌类型(branded types,TypeScript 中通常是& { readonly __brand: "UserId" }这样的交叉类型)让编译器拦住参数传错。
记住八个字:创建时验证一次,下游信任类型。
边界验证:外部数据解析前都是"无类型"
RPC 响应、JSON、命令行参数、环境变量、数据库行……外部数据在解析之前都是"无类型"的。正确姿势是在每个边界放置解析函数,把无结构输入转成类型化模型;跨过边界后就信任内部类型,不要在调用链深处重复验证。
TypeScript 里这意味外部输入应标为unknown而不是any——前者逼你收窄,后者只是闭眼。
不要对类型系统撒谎
as断言、!非空断言、绕过编译器的断言函数,都是潜伏的运行时崩溃。如果编译器无法证明某个事实,就亲自证明它(验证、收窄、细化模型);证明不了,就承认这个断言是风险点。
穷举检查交给编译器
对联合类型做匹配时,让编译器保证"新增变体但没人处理 → 编译失败"。TypeScript 的惯用法是在switch的default分支写const _exhaustive: never = x;。这样下个月有人加变体时,编译器会明确告诉下一个人(或下一个 AI Agent)该在哪里补 case。
从权威 Schema 派生类型
如果 protobuf、OpenAPI 或数据库迁移文件已经定义了某种形状,从它派生类型,而不是手写一个平行接口。重复的形状必然随 schema 演进而漂移。
克制:只在出现"部分性"时才强化类型
类型系统的职责是跟踪每个调用点必须处理哪些情况,而不是把数据描述得越精确越好:
- 空列表求和是 0 → 普通
T[]就够 - 空列表取首元素无解 → 输入必须是"非空版本"
优先全函数(total functions)。当某处出现运行时断言、空值检查或"这里绝不应该发生"的 throw,就标记出类型太弱的地方——把检查上提到类型里,然后停手。
代码评审时的 6 问自检清单 📋
原文附带 6 个自检问题,可直接用于 review:
- 你能否写一条注释解释"这种字段组合何时合法"?能 → 类型太松,拆成联合。
- 两个函数参数同为原始类型、含义却不同?→ 加品牌。
- 那个
any、as是从哪来的?→ 追到边界去验证。 - 新增变体时,编译器会告诉下一个人去哪补 case 吗?→ 不会就补穷举检查。
- 这个类型是不是复制了别的文件拥有的形状?→ 改为派生。
- 我强化类型是为了让操作变全,还是单纯想更精确?→ 后者就保持简单类型。
在 pstack 中如何自动应用这些原则 🤖
pstack 是把 Poteto 的 pstack 移植到 Claude Code、Codex、Pi 等运行时的技能栈(skill stack)。这些原则不需要你手动背诵——typescript-best-practices技能在读写.ts文件时会自动加载,并优先应用类型系统纪律。相关路径:
| 资料 | 路径 |
|---|---|
| 原则原文(8 个模式 + 6 问自检) | principle-type-system-discipline/SKILL.md |
| TypeScript 规则速查表 | typescript-best-practices/SKILL.md |
| 每条规则配套示例 | references/patterns.md |
| 原则总索引(架构组) | poteto-mode/SKILL.md |
想体验完整工作流,直接对 Agent 说一句话即可,例如:"Use poteto-mode to fix the search filter resetting when I change pages",它会自动选剧本、委派执行并给出失败与通过的证据。
小结
一句话记住原则 09:类型系统不是 lint 工具,是你的第一道运行时。把非法状态定义到不存在、给语义值打品牌、在边界解析外部数据、不对编译器撒谎、穷举交给编译器——绝大多数"不可能"的运行时崩溃,会在编译期就消失。
【免费下载链接】pstack-claudeClaude Code, Codex, Copilot, Pi, OpenCode, Gemini, and Prime Agent versions of Poteto's pstack. Rigorous agent workflows with Cursor primitives translated for other harnesses.项目地址: https://gitcode.com/GitHub_Trending/ps/pstack-claude
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考