关于 strong-types
strong-types 技能为使用具备类型系统语言的开发者提供编码与审查准则,重点是消除类型歧义、明确边界数据结构和空值策略。它按语言提供针对性参考,并强调遵循现有检查器配置。
主要功能
- 01为函数参数和返回值明确类型
- 02用命名类型表达跨边界数据
- 03显式处理空值与非法状态
- 04避免类型逃逸、强制转换和抑制
- 05按语言查阅针对性参考
- 06通过项目配置的检查器验证
适用场景
- 01实现或重构类型化代码
- 02审查变更中的空值与类型歧义
- 03将外部输入解析为明确的数据结构
- 04在既有检查器设置下验证改动
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jpcaparas/ skills --skill strong-types安装后在 Claude Code 中输入/strong-types确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
AGENTS.mdmetadata.jsonREADME.mdskill-card.prompt.mdagents/openai.yamlassets/.gitkeepevals/evals.jsonreferences/go-rust-swift.mdgotchas.mdgradual-languages.mdjvm-and-dotnet.mdphp.mdprinciples.mdpython.mdreview-rubric.mdsource-notes.mdtypescript.mdscripts/analyze_type_strictness.pytest_skill.pyvalidate.pytemplates/type-review.md详细介绍
强类型
编写、编辑和审查代码,确保程序中的每个值在任何时刻都有一个明确可知的类型。类型歧义就是缺陷:如果作者不运行代码就无法说出某个表达式的类型,那么代码就尚未完成。
被动触发
每当编码任务涉及具有真实类型系统的语言时,都在后台加载此技能,包括静态类型语言(C#、Java、Kotlin、Swift、Go、Rust、TypeScript)以及配备成熟工具的渐进式类型语言(PHP、Python、搭配 Sorbet 的 Ruby、搭配 typespecs 的 Elixir)。将此技能应用于新代码、编辑、重构、审查和实现计划。
保持适度:对于小型编辑,默默检查类型歧义,只提及会改变实现方式的决策。条件允许时,也加载 {{ skill:maintainable-code }},用于提升通用代码质量。如果语言或代码库没有可用的类型系统,不要强行套用;请查阅 references/gradual-languages.md,了解合适的替代方案。
典型问题
$image = $location->preview ?? $location->banner ?? $location->thumbnail;
这一行留下了四个未解答的问题:每个属性是什么类型,其中哪些可为空以及原因是什么,之后的 $image 是什么类型,以及整个链式表达式是否仍可能为 null。强类型代码会在签名中回答这四个问题:
final class Location
{
public function primaryImage(): Image
{
return $this->preview ?? $this->banner ?? $this->thumbnail
?? throw new MissingImageException($this->id);
}
}
兜底逻辑仍然存在,但它被放在一个有名称、有类型的方法中,具有显式返回类型、明确的全空情况处理策略,以及领域名称(primaryImage),而不是在每个调用点内联重新推导。这种转换正是此技能的核心做法。
决策树
你正在处理什么?仅当任务需要特定语言的详细信息时,才将这些参考资料用作针对性辅助,而不要把它们当作每次编辑都必须学习的教程。保留框架和接口的精确类型契约;如需使用逃逸机制,应将其限制在必要范围内并说明理由,不要以不兼容的方式缩窄签名。
-
PHP(Laravel、Symfony、原生 PHP): 详情请查阅
references/php.md。要求使用declare(strict_types=1)、类型化属性、类型化签名、枚举、用 DTO 替代关联数组,以及 PHPStan/Psalm 级别的泛型注释。 -
TypeScript 或可编译 TypeScript 的 JS 代码库: 详情请查阅
references/typescript.md。要求使用strict: true,禁止any和@ts-ignore,在边界处使用unknown并进行解析,使用可辨识联合类型和穷尽式switch。 -
Python: 详情请查阅
references/python.md。要求为所有签名添加注解,使用 mypy/pyright 严格模式,用 dataclasses/TypedDict/pydantic 替代原始字典,用Enum/Literal替代字符串。 -
C#、Java 或 Kotlin: 详情请查阅
references/jvm-and-dotnet.md。要求使用可空引用类型(C#)、封闭层次结构和记录、穷尽式模式匹配,并避免未经检查的类型转换和!!。 -
Go、Rust 或 Swift: 详情请查阅
references/go-rust-swift.md。禁止使用interface{}/any逃逸机制、在生产路径中使用unwrap(),以及强制解包;优先使用 newtype 和携带关联数据的枚举。 -
原生 JavaScript、Ruby、Elixir、Lua、shell 或其他动态类型环境: 详情请查阅
references/gradual-languages.md,了解替代方案。仅在有帮助时建议使用类型工具,不要重写项目或违背其惯例。 -
审查差异或计划: 适用时使用
references/review-rubric.md。优先指出可能导致运行时故障的歧义:可空值泄漏、通过any混淆类型、字符串化状态以及静默类型转换。 -
不确定从哪里开始: 先阅读
references/principles.md,然后运行下方的类型歧义检查。
快速参考
| 歧义迹象 | 默认操作 |
|---|---|
对象属性上的 ?? / ` | |
any、mixed、interface{}、Object、未类型化的 dict/array | 替换为具体类型、泛型参数、联合类型,或使用 unknown 并进行解析 |
| 未类型化的函数签名 | 先添加参数和返回类型,再处理函数体;void/None 也算 |
| 跨边界传递的关联数组 / 原始字典 / 匿名对象 | 定义具有命名类型化字段的 DTO、dataclass、record、struct 或 TypedDict |
魔法字符串状态("pending"、"active") | 替换为枚举、密封类型或 Literal 联合类型,并进行穷尽处理 |
| “以防万一”而到处传递的可空值 | 在边界处一次性收窄类型;向内传递时保持非空类型 |
| 外部输入(HTTP、JSON、环境变量、数据库行、CLI 参数) | 在边界处解析为类型化结构;绝不让原始载荷形状继续传递 |
抑制类型错误(@ts-ignore、# type: ignore、@phpstan-ignore) | 修正类型,或在行内注明确切错误代码和原因 |
用于让检查器不再报错的强制转换(as any、(array)、!!、未经检查的强制转换) | 替换为类型收窄、类型守卫,或能够明确失败的解析步骤 |
| 用布尔参数对编码状态机 | 替换为一个枚举 / 密封类型,从而杜绝非法组合 |
| 同一变量被重新赋值为不同类型 | 拆分为多个各自只有一种类型的变量 |
| 编译器 / 检查器设置宽松 | 新代码保持作用域内严格;旧代码迁移范围明确时使用基线 |
核心规则
- 每个函数签名都必须完整注明类型:每个参数、返回类型,以及语言支持跟踪的抛出/引发错误类型。未注明类型的签名就是未完成的签名。
any、mixed、interface{}、Object以及未注明元素类型的dict/array都是逃生口,而不是类型。每次使用都需要书面说明理由;默认应使用具体类型、泛型,或使用unknown后进行解析。- 让非法状态无法表示。优先使用枚举、封闭层次结构、可辨识联合和非空字段,而不是布尔值、魔法字符串和所有字段都可选的记录。
- 解析,而非验证。只在边界处将不可信输入转换为类型化结构一次,之后只传递该类型化结构。只检查结构形状却不改变类型,是在做无用功。
- 跨越函数边界的数据必须使用具名类型。关联数组、原始
dict和匿名结构仅可用于私有的本地数据传递;能不用就不用。 - 可空性是设计决策,而非默认设置。只有当“缺失”是实际领域状态时,字段才可为空,并且每个可空值读取都必须有且仅有一个负责处理它的所有者。调用点上的备用值链意味着缺少负责处理的所有者。
- 一个变量只对应一种类型。将变量重新赋值为其他类型会破坏类型推断和读者的信任。
- 类型转换会转移风险,而不会消除风险。优先使用检查器能够验证的类型收窄(类型守卫、模式匹配、
instanceof、穷尽式switch)。不会明确失败的类型转换,只是一个等待别人相信的谎言。 - 提高检查器的严格程度,而不是关闭它。对范围明确的新代码,严格的编译器标志和严格的分析器级别是基准;在旧代码中采用时,使用基线/逐步收紧文件,绝不使用一刀切的抑制。普通编辑不需要在整个项目范围内推广工具。
- 泛型用于在边界之间保留类型。会擦除类型的容器、存储库或辅助程序(
List<Object>、Collection<mixed>)会迫使每个调用方进行类型转换;应改用参数化类型。 - 穷尽性由编译器强制执行,而不是靠注释。对封闭类型执行的每个
switch/match都必须处理所有情况;新增情况时,否则编译必须失败。 - 不要强求语言无法实现的要求。在纯 JS、未配备工具的 Ruby、Lua 或 shell 中,应采取防御性写法、记录结构形状,并在适用时建议而非强制使用类型工具。如需了解替代方案,请查阅
references/gradual-languages.md。
类型歧义检查门槛
在类型化语言中完成更改前,请检查适用的门槛。这是辅助推理的工具,并非要求在报告中逐行列出:
| 门槛 | 通过条件 |
|---|---|
| 签名 | 每个新增或修改的函数都显式注明参数和返回类型 |
| 逃生口 | 没有新增 any/mixed/interface{}/未注明元素类型的集合,除非附有行内说明 |
| 边界 | 外部输入在边界处解析为具名类型;原始负载结构不会向内传递 |
| 可空性 | 每个可空值都只有一个负责处理的所有者;调用点没有重复的 ??/` |
| 状态 | 封闭状态集合使用枚举/联合类型,并由编译器检查是否穷尽处理 |
| 类型转换 | 没有仅为绕过检查器而进行的类型转换;改用类型收窄或解析 |
| 抑制 | 没有新增一刀切的错误抑制;任何针对性抑制都注明错误和原因 |
| 严格性 | 新文件达到项目支持的最严格检查器级别;若推广范围涵盖旧代码,则配置逐步收紧机制 |
| 数据结构 | 跨边界数据使用 DTO/记录/数据类,而非关联数组或原始 dict |
| 合比例性 | 未将非类型化语言的代码强行转换为类型化代码;相关指导仅供参考 |
操作工作流
-
确定类型体系。 在编写代码前,识别语言、检查器(tsc、PHPStan、Psalm、mypy、pyright、编译器标志)以及配置的严格程度。达到或超过该标准;绝不要低于它。
-
先为边界添加类型。 在实现逻辑前,先定义进入和离开此次改动的数据传输对象(DTO)、枚举和签名。形状确定后,顺利路径就很容易实现。
-
排查差异中的歧义。 扫描回退链、逃生舱类型、未声明类型的签名、魔法字符串和类型转换。每一项都必须补上具体类型,或用一行说明理由。
-
在源头解决可空性问题。 遇到
?? $b ?? $c链时,查明每一环为何可空,确定由谁负责(访问器、工厂、数据库默认值、解析步骤),并将这条链收敛为一个命名明确且有类型的成员。 -
使用检查器验证。 在配置的级别上,使用项目支持的项目级或包级调用方式运行检查器;如果直接传入任意修改过的文件会绕过配置或不受支持,就不要这么做。确保新增代码在限定范围内保持严格;如果存在既有故障或检查不可用,应如实报告,切勿放宽设置。
-
如实报告。 报告重要的类型或可空性决策、验证结果以及未解决的歧义。微小改动无需列出每种类型或每道检查关卡。
可选辅助工具
将辅助工具用作快速歧义扫描器,而非裁决依据:
python3 scripts/analyze_type_strictness.py /path/to/project
python3 scripts/analyze_type_strictness.py /path/to/project --json
它会标记疑似回退链、逃生舱类型、未声明类型的签名、被抑制的类型错误、缺少 strict_types 声明以及非严格的 TypeScript 配置。扫描结果干净,并不能证明代码具有强类型;扫描结果嘈杂,也不能证明代码有误。
指南不再适用时
如果捆绑的类型模式失效或与惯用写法冲突,请检查项目的编译器/检查器版本以及官方语言或框架文档。使用不会放宽项目设置的最小可类型检查示例。提出对规范技能进行修正或删除的建议,并提供来源/版本及反例;如证据不可用,应如实报告,而非编造语法。不要默默更新已安装的副本,也不要将本地修复扩大为类型迁移。
阅读指南
| 需求 | 阅读 |
|---|---|
| 通用强类型原则和黄金参考 | references/principles.md |
| PHP:strict_types、类型化属性、枚举、DTO、PHPStan/Psalm 泛型 | references/php.md |
| TypeScript:严格标志、禁止 any、联合类型、在边界处解析 | references/typescript.md |
| Python:注解、严格模式下的 mypy/pyright、数据类、Protocol | references/python.md |
| C#、Java、Kotlin:可空引用、记录类型、密封类型、穷尽匹配 | references/jvm-and-dotnet.md |
| Go、Rust、Swift:逃生舱、新类型、枚举、可选值 | references/go-rust-swift.md |
| 何时以及如何避免强行添加类型 | references/gradual-languages.md |
| 按严重程度优先审查类型差异 | references/review-rubric.md |
| 常见陷阱和反模式 | references/gotchas.md |
| 研究来源和改编说明 | references/source-notes.md |
注意事项
- 添加类型注解不等于强类型。
function f(mixed $x): mixed虽然有注解,但仍然完全含糊不清。 - 靠强制类型转换让检查器变绿,比没有检查器还糟:类型系统现在会主动断言错误的内容。
- 每个边界对应一个 DTO,不代表每个函数都要一个 DTO。过度拆分类型会造成大量映射工作;在确实含义相同的层之间复用领域类型。
- 一次性大规模改造无法顺利补上严格性。循序渐进:新文件启用严格检查,旧文件维持基线,再逐步清除基线。
- 回退链有时掩盖的是数据问题,而不是代码问题。如果存在三个图像列只是因为没人规范化架构,那么类型化访问器只是权宜之计——记下架构修复事项。
unknown(TS)和object是处理不可信数据的安全默认类型;any和mixed则不是。前者会强制进行解析;后者会悄无声息地污染所有下游内容。- 类型体操本身也是一种异味。如果需要一大段条件泛型才能表达某个类型,就简化设计,而不是炫耀类型系统。