关于 principle-type-system-discipline
一项适用于多种语言的开发技能,指导开发者利用类型系统排除无效状态,并在编译期发现遗漏分支。面向使用静态类型语言的开发者,强调可靠建模、边界解析与穷尽处理。
主要功能
- 01让非法状态无法表示
- 02为语义不同的基础类型分别定义类型
- 03在边界解析无类型数据
- 04避免绕过类型检查的强制转换
- 05要求穷尽处理所有变体
- 06从权威模式派生类型
适用场景
- 01建模互斥的应用状态
- 02防止含义不同的 ID 混用
- 03校验 API 或配置中的数据
- 04检查新增变体是否都已处理
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ hachej/ boring-stack --skill principle-type-system-discipline安装后在 Claude Code 中输入/principle-type-system-discipline确认已加载。
附带文件LICENSE
详细介绍
类型系统纪律
类型检查器是一种证明助手。使用它在编译时消除不可能的状态、不匹配的基本类型以及未处理的变体。类型允许忽略的情况,会成为编译器本可以阻止的运行时故障。与其不断增加处理逻辑,不如让错误和特殊情况从一开始就无法出现。无法表示的状态、全函数和接口重设计(下文介绍的模式)都是可用的工具。
适用于任何有类型的语言。typescript-best-practices 等技能会结合特定语法阐释这些原则。
这些模式:
- 让非法状态无法表示。 将变体建模为和类型:TypeScript 中的判别联合、Rust/Swift/Kotlin 中带载荷的枚举、Scala 中的密封类,以及 Haskell/OCaml 中的代数数据类型。不要把状态建模为一组可选字段,让相互矛盾的组合也能通过编译。一种微妙的反模式是:
{ completed: boolean; completedAt?: Date }允许completed: true; completedAt: undefined,这毫无意义。应从单一来源派生布尔值,例如completedAt !== null,或者显式建模各个变体,例如{ kind: 'open' } | { kind: 'done'; at: Date }。如果出现了“等等,这种组合真的可能发生吗?”的问题,说明类型定义得太宽松了。 - 类型是构造方式,而非限制条件。 从你想要的值出发构造类型,而不是先采用宽松的类型,再通过检查剔除不符合条件的值。看似需要精化类型的不变量,通常只需换一种构造方式即可表达。非空列表由一个头部元素和其余元素组成,而不是附带长度检查的列表。有效的时间范围由起始时间和持续时长组成,而不是两个必须保持顺序的时间戳。没有哪种表示方式天然更优。只要换个角度理解,成对元素的列表就是长度为偶数的列表;因此,应选择无法构造非法值的形状,并在其上提供调用方所需的接口。
- 为语义基本类型添加品牌。
UserId和OrderId底层都是字符串,但不应能互换使用。可以使用 Rust 中的新类型、Swift 中的不透明类型、Kotlin 中的值类、Haskell 中的幻影类型,以及 TypeScript 中带品牌的交叉类型。在创建时验证一次,后续就信任该类型。 - 外部数据在解析前都没有类型。 RPC 载荷、JSON、IPC 消息、CLI 参数、配置文件、环境变量和数据库行皆是如此。在每个边界处都应设置解析函数,将非结构化输入转换为有类型的模型。关于验证应放在哪里,请参阅 boundary-discipline 原则技能。
- 不要对类型系统撒谎。 绕过编译器的强制类型转换、不安全的强制转换和断言函数,都是潜在的运行时崩溃。如果编译器无法证明某个事实,就应证明它(进行验证、缩小范围或精化模型),或者承认强制转换存在风险。
- 穷尽匹配是编译器的职责。 对和类型进行匹配时,如果新增变体却未处理,编译器必须报错。使用语言提供的惯用方式:TypeScript 中绑定为
never类型、Rust 中不带注解的match、Haskell 中的-Wincomplete-patterns,以及 Kotlin 中针对密封类的穷尽匹配检查。 - 从权威模式派生类型。 如果 protocol buffer、OpenAPI 规范、GraphQL 模式、数据库迁移或设计系统令牌文件定义了某种结构,就应从中派生类型,而不是手动另行定义一套平行类型。请参阅 encode-lessons-in-structure 原则技能。
- 只在出现非全函数性时增强类型。 运行时断言、空值检查或“此处绝不应发生”异常,都标记着类型过于薄弱的位置。将检查上移到类型中,然后就此打住。类型系统的职责是追踪每个使用位置必须处理的情况,而不是尽可能精确地描述数据。优先使用全函数。空列表的
sum为 0,因此接受普通列表。空列表的head没有答案,因此要求非空列表。
这些测试:
- “我能否写一条注释,说明这种字段组合何时有效?”如果可以,说明类型过于宽泛。将其拆分为和类型。
- “我的两个函数参数是否具有相同的基本类型,但含义不同?”为它们添加名义类型标记。
- “这个
any、这个as、这个assertNotNull是从哪里来的?”追溯到边界,并在那里进行验证。 - “如果下个月新增一个变体,编译器会告诉下一个 Agent 应该在哪里添加分支吗?”如果不会,说明匹配并不穷尽。
- “这个类型是否重复定义了另一个文件所拥有的结构?”改为派生。
- “我是在强化这个类型以确保操作全定义,还是只是为了让类型更精确?”如果原本不会发生 panic,就保留普通类型。
评论
支持 Markdown