关于 top-down-typescript
面向开发者和编码 Agent 的 TypeScript 编码风格 Skill,规定代码组织、类型、导入与可维护性实践。其自上而下的排列方式让入口先于依赖的辅助函数和类型呈现。
主要功能
- 01自上而下组织文件
- 02TypeScript 类型约定
- 03ESM 导入规则
- 04函数与类的设计指导
- 05代码及变更说明规范
适用场景
- 01统一 TypeScript 项目的编码约定
- 02检查代码组织和类型选择
- 03指导编码 Agent 实施修改
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ paleo/ skills --skill top-down-typescript安装后在 Claude Code 中输入/top-down-typescript确认已加载。
详细介绍
自上而下的 TypeScript 编码风格
通用规则
- 不应保留无用(未使用)的代码(YAGNI 原则)。
- 不要连续使用多行空行。
- 修改 linter 规则前,必须先进行讨论。
- 代码中不应包含注释掉的代码,除非注释说明了原因。
代码组织
先写用法,再写实现。例外情况:涉及继承时,如果一个接口 extends 另一个接口,应先写父接口。
-
自上而下组织代码:每个文件都应像一个故事,从入口一路展开到最底层。读者先看到最高层的内容,再逐步了解其依赖项:
// 1. Imports import { ... } from "..."; // 2. Module-level constants and variables (exported first, then internal) export const PUBLIC_CONST = ...; const INTERNAL_CONST = ...; // 3. Shared types — main type first, then types it references export interface MainType { detail: DetailType; } export interface DetailType { ... } // 4. Entry-point (exported) function export function doThing() { stepOne(); stepTwo(); } // 5. Internal functions called by the entry point, in call order function stepOne() { stepOneHelper(); } function stepOneHelper() { ... } function stepTwo() { ... } -
模块级常量和变量(文件顶部的
const、let、var值声明,包括导出的和内部的)必须紧跟在导入语句之后,放在所有类型定义、函数或类之前。读者会先看到它们,并将其视为文件的配置入口。 -
函数:先写调用方,再写它所调用的函数,依此递归。辅助函数应放在其调用方的_正下方_,而不是集中放在文件底部。如果多个同级函数都会调用某个辅助函数,应将其放在第一个调用方之后。
-
类型:先写主要(顶层)类型,再递归写它所引用的类型。遵循与函数相同的自上而下规则。
-
仅与单个函数(或类或其他声明)关联的类型——即只在某个签名中使用的类型,例如仅供
MyComponent使用的MyComponentProps接口——必须紧挨着放在该声明之前,而不是放在顶部的类型区块中。 -
导出与否本身不是排序依据:某个
function即使被export,也不会因此被提到最前面——它的位置由调用它的代码决定。文件的入口通常会被导出,因此往往排在前面,但这是自上而下规则的结果,而非规则本身。
代码质量标准
- 从首次实现开始,就力求采用优雅的解决方案
- 避免冗余操作,尤其是图像转换等开销较大的操作
- 避免重复的代码和逻辑
- 在函数之间传递先前计算出的值,而不是重新计算
- 尽可能使用提前返回来简化代码流程
- 对于会离开当前流程的代码(
throw、return、continue、break),如果能写在一行内,就写在一行内(例如,使用if (!condition) return false;,而不是多行格式) - 使用能清楚表达意图的函数名和变量名,减少对注释的需求
- 保持函数精简,各自只负责一项职责
- 避免使用
any;花时间寻找合适的类型。如果找不到,始终在any后插入/* FIXME */。例如:let myVariable: any /* FIXME */;。 - 只导出从其他地方导入的函数(或变量、类)。默认情况下不要导出。
- 始终导出在本地声明、用于公共签名的类型和接口,即使目前还没有文件导入它们。
- 当命名类型描述公共结果时,将其用作返回类型(异步函数使用
Promise<T>)。
导入
- 始终使用 ESM 导入语法(例如,使用
import { X } from "y.js",而不是require)。 - 避免模块之间的循环导入。
TypeScript、JavaScript
- 不要使用
enum和namespace。 - 优先使用
const,而不是let。 - 优先使用
undefined,而不是null。 - 优先使用
??,而不是||。 - 优先使用
++i和--i,而不是i++和i--。 - 优先使用
new Error(),而不是Error()。 - 在顶层,优先使用
function和class声明语法,而不是将它们创建为常量。 - 顶层函数、类和接口之间应留一个空行。
- getter 或 setter(EcmaScript 5 语法)的实现绝不能抛出异常。
- 优先使用
interface声明,而不是type别名。 - 泛型参数优先使用单个大写字母,例如
T、K等。 - 不要区分属性不存在和属性值为
undefined的情况。 - TypeScript 联合类型中的字符串字面量值使用 camelCase(例如,使用
"normal" | "gracefulShutdown" | "backupMode",而不是"normal" | "graceful-shutdown" | "backup-mode")。 - 除非确实需要空字符串,否则不要将空字符串用作默认值。如果变量可能没有值,请使用
undefined;如果缺少值意味着出现问题,则抛出错误。 - 绝不能通过强制转换为布尔值来检查字符串、数字、布尔值(以及值为字符串或数字的标识符)是否存在。请使用与
undefined或null的显式比较。 - 对象和数组的存在性检查可以使用布尔强制转换。
- 如果
undefined是默认值,就不要显式赋值或返回undefined。使用return;而不是return undefined;,使用let myVariable;而不是let myVariable = undefined;。有意设置值时,可以显式传入undefined。 - 避免使用
as any或任何形式的类型断言。始终尽力找到正确的类型。例外:类型不正确或确实未知时,可以使用,但需添加行内注释说明理由。 - 除从包的 index 文件导出外,不要重新导出。
- 避免使用行内
import("some-package-or-module").SomeType;优先在文件顶部直接导入。 - 避免使用行内
await import("some-package-or-module");优先在文件顶部静态导入。例外:有正当理由时可以使用,但需添加行内注释说明理由。
OOP
- 优先使用工厂函数,而不是类。
- 优先编写带有上下文对象的函数,而不是使用类。
- 避免类继承,但框架要求继承的情况除外。
添加包依赖
添加依赖或开发依赖之前,先搜索代码库并复用已在使用的版本。如果没有找到,请使用默认安装命令安装最新版本。
Commit、PR/MR 和 changeset 消息
绝不要在 commit、PR、MR 或 changeset 消息中添加 AI 署名,例如 Co-Authored-By: …、"Generated with …" 或类似内容。
Changeset 消息
面向已在使用该项目、希望了解变更内容的用户,用简短的文字说明。只提及对他们有实际作用的内容;省略原因和内部细节。始终使用一个简短段落。
- 新功能:用几个词概括功能。
- 扩展功能:如果标题显而易见,就为其命名;否则使用 "Improved the {X} feature."。
- 没有实际作用的变更(例如文档或重构):简洁描述,例如 "Improved documentation about {topic}."。
如果一个版本同时包含有实际作用和没有实际作用的变更(例如大型重构和一个小功能),只提及有实际作用的变更。只有在没有其他内容可提及时,才提及没有实际作用的变更。
版本升级
除非明确要求,否则绝不要将包从 0.x.x 升级到 1.0.0。主版本号为 0 时可以接受破坏性变更。
提升代码质量
SRP - 单一职责原则
可以将其视为叙事分解:调用方读起来像一段说明要发生什么的文字;每个辅助函数则展开其中的一句话。
例如,这段代码:
export function myFunction() {
// Check something
// ... 20 lines ...
// Do something
// ... 20 lines ...
}
……应重构为:
export function myFunction() {
checkSomething();
doSomething();
}
function checkSomething() {
// ... 20 lines ...
}
function doSomething() {
// ... 20 lines ...
}
指南:
- 始终将子函数写在调用它的函数_之后_。
- 除非函数需要从源文件外部导入,否则不要导出函数。
- 拆分代码时应用_单一职责原则_:一个函数只负责一件事。
- 函数实现尽量不要超过一屏的高度(约 50 行)。
- 保持代码整洁、易于理解,而不是添加解释性注释。
DRY - 不要重复自己
每当发现重复逻辑时,都应花时间将其重构为可复用函数。
YAGNI - 你不会需要它
不要保留未使用的代码,例如变量、函数、实现等。
警示信号
- 将空字符串或零作为回退值通常没有充分理由:检查每个
?? ""和?? 0,确认这是有意为之。否则,应了解类型并找到优雅的修复方式。- 注意:当 UI 要求使用空字符串时,
?? ""是合理的用法。
- 注意:当 UI 要求使用空字符串时,
- 类型断言(
as SomeType)通常意味着对类型的理解有误。 - 避免使用
any,并找到合适的类型。确实需要使用any时,请添加注释说明原因。 - 如果可以导入实际类型,就不要使用
ReturnType<T>或Parameters<T>。 - 如果可以导入实际类型,就不要使用
SomeType["someMemberName"]。
删除不必要的注释
- 关于注释:越少越好。注释是供熟练开发者阅读的。每条注释都必须精炼、简洁、直截了当。每个词都应经过仔细斟酌和选择。
- 删除与代码本身重复的注释。
- 只保留用于说明技巧性变通、待办事项或特殊情况的行内注释,或者用于解释代码结构无法清楚表达的目的。
- 不要用注释为当前正在处理的任务作说明。
以下注释必须删除:
// Create a new task
createANewTask();
其他显而易见、必须删除的行内注释示例:
// Validate that there is a file
if (!file) throw new ApiError(400);
// Validate and parse the request body according to the expected schema
const validated = UploadBodyAT.assert(body);
只有在能提供有意义的信息时,才应使用 JSDoc 注释。以下注释示例中的所有内容都显而易见,因此必须整体删除:
/**
* This function adds two numbers
* @param a - The first number
* @param b - The second number
* @returns The sum of the two numbers
*/
function addTwoNumbers(a: number, b: number) {
return a + b;
}