关于 typescript-best-practices
为 TypeScript 和 TSX 修改提供编码与重构规范,适用于需要一致类型边界和清晰代码的开发者。内容涵盖数据校验、函数设计、复用、注释和重复代码检查。
主要功能
- 01用 Zod 校验边界 JSON
- 02保持函数小巧且可组合
- 03复用共享工具与不变量
- 04明确区分边界数据和领域类型
- 05通过 jscpd 检查重复代码
适用场景
- 01修改 TypeScript 或 TSX 文件
- 02审查或修复代码变更
- 03重构重复的数据转换逻辑
- 04统一团队类型与边界处理规范
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ justbecauselabs/ agent-skills --skill typescript-best-practices安装后在 Claude Code 中输入/typescript-best-practices确认已加载。
附带文件references/review-and-fix-changes.md
详细介绍
TypeScript 最佳实践
对任何 .ts 或 .tsx 修改,默认遵循本指南。
保持本文档实用。记录代码库实际采用的 TypeScript 标准。
参考资料
- 对工作区、分支、提交或提交范围进行审查或审查并修复时,请阅读
references/review-and-fix-changes.md,并将其作为工作流程,同时将本文档作为 审查标准。
目标
- 为 TypeScript 的编写和重构提供统一的基准。
- 让代码类型明确、易于阅读和修改。
- 减少 AI 式冗余代码,例如防御性清理、含糊的抽象、重复转换和脆弱的回退逻辑。
快速指南
- 在边界处使用 Zod 验证无类型的 JSON。
- 保持函数小巧且可组合。
- 将边界数据结构与内部领域数据结构分开。
- 只转换一次外部数据,然后让内部代码保持严格、简洁。
- 除非有实际的边界或产品需求,否则不要规范化值。
- 保留原始来源值;必要时添加派生的内部键。
- 对稳定领域数据中有意义的缺失值使用
null。 - 对可选输入和原始边界载荷字段保留使用
undefined。 - 添加新辅助函数之前,先复用现有辅助函数。
- 对字符串、数字、URL、数组、字典、去重、过滤或类似基础逻辑,先在代码库中查找现有的共享辅助函数位置,并在那里添加共享逻辑,而不是将其埋在功能代码中。
- 提取共享不变量和边界适配器;不要臆造通用清理工具。
- 重构后、审查前,使用
jscpd快速检查重复代码。 - 除非用户明确要求添加代码注释,否则不要添加或扩充代码注释。
优先级顺序
- 优先遵循项目特定约定。
- 明确标注类型并清晰定义边界。
- 添加新抽象之前,先复用现有辅助函数和不变量。
- 仅当确实出现重复时,才提取重复的边界适配器或不变量。
- 清晰优先于巧妙。
- 只有在正确性、边界和复用问题都解决后,才考虑风格偏好。
核心规则
1. 在边界处验证并转换无类型数据一次,然后保持领域类型严格。
错误示例:
type Issue = {
id: string;
attemptCount: number;
};
function parseIssue({ body }: { body: string }): Issue {
return JSON.parse(body) as Issue;
}
正确示例:
import { z } from "zod";
const issueSchema = z.object({
id: z.string().min(1),
attemptCount: z.coerce.number().int().nonnegative(),
});
type Issue = z.infer<typeof issueSchema>;
function parseIssue({ body }: { body: string }): Issue {
return issueSchema.parse(JSON.parse(body) as unknown);
}
2. 保持函数小巧且可组合。函数接受多个由调用方提供的参数时,使用 params 对象。
- 编写函数时,优先使用
function name()声明,而不是const name = () => {}。
错误示例:
function buildWorkspaceLabel(issue: Issue, ownerName?: string): string {
const normalizedState = issue.state.trim().toLowerCase();
const normalizedOwner = ownerName ? ownerName.trim() : "unassigned";
const slug = `${issue.identifier}-${normalizedState}-${normalizedOwner}`
.replaceAll(/[^\w-]+/g, "-")
.replaceAll(/-+/g, "-")
.replace(/^-|-$/g, "");
if (!slug) {
return "workspace";
}
return slug;
}
正确示例:
function buildWorkspaceLabel(params: {
issue: Issue;
ownerName?: string;
}): string {
const stateKey = buildStateKey(params.issue.state);
const ownerKey = buildOwnerKey(params.ownerName);
return buildWorkspaceSlug({
identifier: params.issue.identifier,
stateKey,
ownerKey,
});
}
function buildOwnerKey(ownerName?: string): string {
return ownerName ?? "unassigned";
}
3. 将字符串、数字、URL、数组、字典、去重和过滤等基础逻辑放入共享辅助函数。
错误示例:
function listUniqueIssueIds(issues: Issue[]): string[] {
return [...new Set(issues.map((issue) => issue.id))];
}
正确示例:
import { unique } from "../shared/collections";
function listUniqueIssueIds(issues: Issue[]): string[] {
return unique(issues.map((issue) => issue.id));
}
如果目前没有共享辅助函数,请先查找现有的共享辅助函数位置。对于字符串、数字、URL、数组、字典、去重或过滤等纯基础逻辑,应在该处添加可复用的辅助函数,而不是将其嵌入功能模块。务必查找 utils/helpers/etc 文件夹
4. 除非用户明确要求,否则不要编写代码注释。
- 这包括行注释、块注释、JSDoc、TODO、注释掉的代码和指令注释。
- 现有注释、相邻代码的风格、lint 规则和文档都不算用户的明确指示。
- 使用精确的名称、类型、小函数和清晰的结构来表达意图。
- 保留准确的现有注释。如果修改使注释过时,除非用户明确要求代码注释,否则应删除,而不是改写。
5. 明确报告边界错误,不要使用防御性默认值。
错误示例:
function parseLimit(value?: string): number {
return Number(value) || 0;
}
正确示例:
const limitSchema = z.coerce.number().int().nonnegative();
function parseLimit(value: string): number {
return limitSchema.parse(value);
}
重构规则
- 添加新辅助函数前,先在仓库中搜索是否已有解决相同问题的辅助函数、转换函数或适配器。
- 对字符串、数字、URL、数组、字典、去重或筛选等基础逻辑,先搜索共享辅助文件,并优先将逻辑添加到其中,而不是引入仅供某个功能使用的辅助函数。
- 如果同一不变量、转换或适配器在多处出现,优先将其提取出来,而不是再添加一份。
- 在确实出现重复后再提取,不要仅因两个代码片段看起来有些相似就提取。
- 共享辅助函数应有明确的领域用途。
- 不要引入混合无关关注点的通用
sanitize*、normalize*、clean*或format*工具函数。 - 如果某个抽象仅用于通用地“清理”值,真正的修复通常应位于类型化边界。
- 重构时保留现有最强类型。不要仅为简化重构而放宽类型。
- 优先使用小型边界适配器和不变量辅助函数,而不是抽象的工具层。
重复代码快速检查
重构后、评审前,像运行类型检查一样,使用 jscpd 快速检查重复代码。从当前仓库根目录运行:
cd "$(git rev-parse --show-toplevel)"
bunx --yes jscpd --format typescript --pattern '**/*.ts' --gitignore \
--min-lines 12 --min-tokens 120 \
--max-lines 5000 --max-size 1mb --reporters console --exitCode 0 .
- 一开始先忽略测试,因为测试夹具常会产生噪声;需要时添加适合仓库的忽略规则,或将匹配范围限定为生产代码。
- 搜索较小的辅助函数重复时,将
--min-tokens调低至60-80。 - 如果可浏览的报告有帮助,请使用
--reporters html,console。 - 将克隆检测与针对语义重复的
rg搜索结合起来:重复的函数名、注释、负载字段或错误字符串。精确克隆工具无法发现拼写不同但逻辑相同的代码。
代码注释规则
- 除非用户明确要求,否则不要添加或扩展代码注释。
- 在此规则中,JSDoc、TODO、解释性代码块、注释掉的代码和指令注释均视为代码注释。
- 不要从现有注释、相邻代码的风格、lint 规则、生成的示例或文档中推断出添加注释的许可。
- 保留准确的现有注释。如果代码更改导致注释过时,应删除,而不是改写,除非用户明确要求添加代码注释。
- 通过准确的命名、强类型、小型辅助函数和清晰的结构,让代码能够自我说明。
工作流程
- 在修改 TypeScript 前,先阅读此技能指南。
- 对于审查或审查并修复任务,在界定工作范围前,先阅读
references/review-and-fix-changes.md。 - 如果项目特定约定与通用指南冲突,优先遵循项目约定。
- 当数据以 JSON 形式进入系统时,在该边界定义 Zod schema。
- 对于已有可信类型的值,避免不必要的清理或强制转换。
- 保持函数小巧且可组合。
- 对于有多个自定义参数的函数,通常优先使用
fn(params: { ... })。 - 对于只有一个参数的函数,直接传递该值,而不是将其包装在 params 对象中,除非有充分的本地理由。
- 除非用户明确要求,否则不要新增或扩展代码注释。
- 如果修改纯粹涉及字符串、数字、URL、数组、字典、去重、筛选或类似基础类型,先在代码库中查找已有的共享辅助函数位置,再将共享逻辑添加到那里。
- 重构后,如果代码库和工具链支持,运行一次快速的
jscpd重复代码冒烟测试。 - 如果代码看起来需要使用
as,请先停下来,寻找更安全的类型处理或验证方式。 - 在新增辅助函数或抽象之前,先在代码库中查找可复用的现有模式。
- 如果多个模块中存在相似逻辑,优先提取共享的不变量或边界适配器,而不是再添加一份副本。
- 如果有子 Agent 可用,在引入新抽象前,使用子 Agent 检查代码库中的现有辅助函数、重复转换和重构机会。
- 对于写入范围明确的多文件重构,使用子 Agent;但不要创建范围过广、可重复使用的工具,除非确实存在重复逻辑。
- 如果某个抽象只是为了通用地“清理”值,请停下来检查真正的修复是否应放在有类型的边界处。
警示信号
JSON.parse(...) as FooNumber(value) || 0- 没有产品层面理由的
input.trim().toLowerCase() - 在业务逻辑中反复使用
foo ?? default - 范围含糊的通用
sanitize*或normalize*辅助函数 - 应拆分为小型可组合函数的大型功能局部辅助函数
- 应放在共享辅助文件中的新功能局部字符串、数字、URL、数组或字典辅助函数
- 多个文件中重复的边界转换
- 没有充分理由却同时包含
null和undefined的领域类型 - 在各层之间传递宽泛的接口或类,而这些层只需要其中一两个方法
- 未经用户明确要求而新增或扩展的代码注释
- 所描述的代码变更后仍未更新的过时注释
审查清单
- 是否使用 Zod 验证原始 JSON,而不是进行类型断言?
- 函数是否足够小且可组合?
- 是否在边界处尽早且只解析一次?
- 是否将边界 DTO 或视图形状一次性转换为内部领域类型?
- 是否在边界处处理外部载荷的差异,而不是让这种差异渗入内部逻辑?
- 代码是否有意区分使用
null和undefined,而不是在领域代码中混用二者? - 代码是否在缺乏明确边界或产品理由的情况下规范化值?
- 如果需要规范化,代码是否保留原始值并添加派生键?
- 仓库中是否已有可复用的辅助函数、转换器或适配器?
- 对于基础字符串、数字、URL、数组、字典、去重或筛选逻辑,代码是否查找并使用了共享辅助文件,而不是在功能模块中重复实现?
- 是否应提取重复的不变量或边界适配器,而不是再次复制?
jscpd冒烟测试或针对性的rg搜索是否发现了值得重构的重复代码?- 是否在本应采用更严格检查或更完善类型的地方使用了
as? - 是否使用联合类型、
Map、Set或as const值等精确类型来建模领域不变量? - 代码是否仅依赖所需的最小能力范围?
- 除非用户明确要求,变更是否避免添加或扩充代码注释?
- 未要求注释时,是否删除了过时的现有注释,而不是重写?
- 代码是否用宽松的默认值或静默回退掩盖了错误的假设?
评论
支持 Markdown