mcpField
T
typescript-write

typescript-write

github.com/metabase开发49.9k2.1k 次安装今天更新
代码仓库

关于 typescript-write

面向代码库贡献者的 TypeScript 和 JavaScript 开发技能,强调严格类型、准确的数据建模,以及必须进行类型检查。

主要功能

  1. 01禁止编辑代码中出现显式或隐式 `any`
  2. 02指导收窄未类型化的值
  3. 03提供类型建模与 API 契约建议
  4. 04要求运行 `bun run type-check-pure` 验证

适用场景

  1. 01编辑 TypeScript 或 TSX,避免引入 `any`
  2. 02准确建模 API 数据与可空状态
  3. 03审查类型断言和函数签名

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/metabase/metabase --skill typescript-write

安装后在 Claude Code 中输入/typescript-write确认已加载。

详细介绍

TypeScript/JavaScript 开发技能

修改前端之前(包括仅修改测试),请先阅读 frontend/AGENTS.md、docs/developers-guide/frontend.md 和以下共享参考资料。新增或修改测试时,请遵循Test authoring。

@./../_shared/development-workflow.md @./../_shared/typescript-commands.md @./../_shared/react-redux-patterns.md

禁止使用 any——硬性规则

  • 新增、移动或以其他方式修改的代码不得包含 any,无论是显式还是隐式。 不得使用 any 注解、as any / as unknown as、会推断为 any 的未类型化参数或返回值,也不得使用隐式 any 的解构或数组/对象字面量。
  • 在边界处拦截未类型化的值。 使用规范类型、带类型的包装器,或结合验证或类型收窄使用 unknown。检查库的默认类型和推断类型:类型检查通过,并不意味着 any 没有传播到你的代码中。例如,将未类型化的 JSON.parse 和响应 .json() 的结果捕获为 unknown;在访问字段或传递给带类型的调用方之前,先进行类型收窄或验证。
  • 为捕获的值添加类型,而不仅仅是为最终结果添加类型。 返回类型注解不会改变参数或解构绑定的推断类型。将未类型化的数据捕获为 unknown,然后在解构或访问字段之前进行类型收窄或验证。
  • 必须进行类型验证。 完成 TS/TSX 修改前,运行 bun run type-check-pure。如果有 TypeScript LSP 工具,也请使用悬停和转到定义检查修改过的符号;否则跳过 LSP 检查。

类型收紧

  • 避免类型断言和宽松的 unknown — 应修复签名。
  • 如果一个函数只需要宽对象的一个字段,就接受该字段 — 而不是整个宽对象。签名正确后,断言往往就会消失。
  • 优先使用 Partial<T>、Pick<T, K>、Record<K, V> 和泛型,而不是类型断言。
  • 让字典的键与数据匹配。 当键已知时,使用有限的键联合类型。开放的字典可以使用索引签名、Record<string, T> 或 Map;索引签名仍然会约束值,而把它们的写法改成 Record<string, T> 并不会让缺失键的访问变得安全。
  • 当一个值原样传递且调用方知道其类型时,优先让 props/组件泛型化(<T>)。
  • 优先使用 unknown 而不是宽松类型,并在使用前进行收窄 — unknown 值会强制在使用点进行守卫。
  • 对必须符合类型但不应被拓宽的对象字面量使用 satisfies(配置对象、查找映射、可辨识字面量)— 优于 : T(会拓宽)或 as T(不安全)。
  • 避免非空断言(!)。优先使用守卫、提前返回或 ?.。仅当非空性可证明为真且范围有限时,才使用 !,并附上注释。
  • 对可能未命中的索引查找进行守卫。 即使推断类型省略了 undefined,数组和字典也可能返回 undefined。使用能保留键/值关系的迭代形式,并且仅当运行时键已知属于声明的类型时,才断言 keyof。
  • 不要进行多余的运行时强制转换 — 不要用 Number() / String() / Boolean() 包装已有类型的值。
  • 类型守卫应放在 frontend/src/metabase-types/guards/。不要在其他地方重新定义它们。
  • 无法避免的类型断言需要真实的理由注释。 metabase/no-unjustified-type-casts 规则接受任何前置注释 — 说明该断言安全的实际原因。绝对不要写旧的 // Unjustified type cast. FIXME 占位符;它只存在于该规则出现之前的类型断言上,复制它会让一个无理由的类型断言蒙混过 linter。如果你无法说清该断言为何正确,那么该断言就是错的 — 修复类型。
  • 让无法避免的断言保持局部。 当把重复或复杂的断言隔离到辅助函数后能更容易地维护其不变量时,就这样做。测试那些为断言提供正当理由的非平凡运行时假设;平凡断言并不自动需要新的辅助函数或测试。不要仅仅为了消除实现错误而削弱公共签名。

类型建模

  • 复用已有类型,不要重新声明。 使用来自 metabase-types/api 的规范 ID 和领域实体类型(FieldId、TableId、ConcreteTableId、SchemaName 等),并以它们作为关键数据结构(new Map<ConcreteTableId, …>())。不要重复生成/API 类型 —— 组合或派生它们(Pick、Omit、索引访问 SomeType["field"]、ReturnType)。在引入本地领域形状之前,先搜索规范声明。如果某个类型没有直接导出,请从所属函数、端点或 hook 派生它,而不是重建其形状。
  • 泛型必须做出实现能够兑现的承诺。 由调用方选择的 get<T>(): T 不能掩盖对数据的未经检查的断言。返回 unknown 并校验,或接受一个能确立 T 的校验器。诸如 function empty<T>(): T[] { return []; } 这样的工厂是有效的;评判实现,而不是 T 在签名中出现的频率。
  • 诚实的类型优先于虚假的精确。 当泛型表达了真实关系时再使用它们。如果复杂类型无法准确建模行为,请选择更简单的类型或使用 unknown 加收窄,而不是断言不受支持的保证。
  • 建模实际的数据契约;保持类型收窄。 对可能不存在的键使用可选 field?: T,仅当键始终存在但值可能为 undefined 时使用 field: T | undefined,对 API 中显式的 null 使用 | null。优先使用领域联合类型,而非宽泛的 string / number / 松散的 Record。
  • 定义或细化类型时,参照 API 实现,以确保它们与实际数据结构匹配。在考虑类型断言时,首先考虑是否应细化类型以匹配实际数据结构。
  • 可选性必须反映缺失。 保持必需字段为必需,可选字段为可选。当应用有有意义的默认值时,对输入进行规范化,而不是仅仅为了消除类型错误。在 API 类型中保留实际的传输形状。
  • 建模内部状态时,将相关的缺失一起表示。 如果多个字段同时存在或消失,请考虑使用可空包含对象或可辨识联合。除非数据确实是那个形状,否则不要重塑原始 API 声明。
  • 设计格式时,优先使用显式的特殊状态。 当诸如 -1 这样的哨兵值掩盖含义时,请使用可空性或具名联合变体。除非行为被刻意更改,否则保留既有的协议哨兵值,或在显式边界处将其规范化。
  • 变体状态使用可辨识联合,并进行穷尽检查。 将“N 种形状之一”建模为带字面量判别式的联合,而不是一堆可选字段,并使用 ts-pattern 的 .exhaustive() 穷尽它,这样新增变体会成为编译错误:
    import { match } from "ts-pattern";
    
    const result = match(status)
      .with({ type: "loading" }, () => <Spinner />)
      .with({ type: "error", error: P.select() }, (error) => <Error message={error.message} />)
      .with({ type: "success", data: P.select() }, (data) => <Content data={data} />)
      .exhaustive(); // Compile-time guarantee all cases handled
    
  • 从常量派生联合类型(as const + typeof/keyof),这样类型和值不会发生偏移。
  • 在无意变更处使用 readonly / 不可变性 —— 组件 props、共享常量、导出的配置以及未变更的参数。对于不进行变更的输入,优先使用 readonly T[] / ReadonlyArray<T>。会变更调用方数据的函数应明确说明该行为。
  • 在可行时构造完整且类型良好的对象。 当对象表达式能避免部分初始化的状态或断言时,优先使用它。当增量构造更清晰且能保持类型的不变式时,增量构造也是可以的。
  • 将 metabase-lib 的不透明类型视为黑盒。 使用 metabase-lib 函数来处理诸如 Lib.Query 之类的类型,而不是转换为其内部表示。
  • 显式地对异步和错误状态进行类型化(使用可辨识联合或数据层的类型化结果)—— 绝不要让加载/错误/空状态隐式存在。

函数签名

  • 在有助于提升稳定性和清晰度的地方,显式声明公共契约。 在共享边界处,必要时标注参数和返回类型;局部值则交由类型推断。当声明需要显式的形状检查时,使用 satisfies 或类型标注。
  • 接受操作所支持的输入,并返回最精确的诚实结果。 在函数内部收窄可避免的不确定性,但在其返回类型中保留有意义的可空性和联合变体。
  • 当位置参数容易混淆时,使用具名选项。 同类型的连续参数是有用的警示信号,而不是自动要求重写清晰 API 的理由。
  • 当 async/await 能澄清控制流或错误处理时,使用它。 直接返回已有的 promise 同样有效;仅凭 Promise 返回类型并不要求添加 async。

null 与 undefined

  • 在源头收窄。如果某个值仅在边缘情况下可选,不要将 undefined 贯穿每一层——在生产方进行防护。
  • 为可选值提供合理的默认值。在消费方使用 ?. 和 ??。
  • 当操作需要存在的值时,收窄可空的列表元素。 当缺失项应被丢弃时,使用类型守卫进行过滤;当其缺失或位置具有意义时,保留它们。
  • 当 X 永远不可能为 null 时,避免非严格的 null 比较(X != null)——使用严格检查或收窄类型。必要时使用 checkNotNull。
  • 对照 API 实现检查实际可空性。找到 API 端点实现,检查该字段是否真的可以为 null。

命名

  • 名称描述实体,而非机制。名称必须反映该值所承载的内容。
  • 使用领域词汇,并在有用时包含单位(timeoutMs、widthPx、temperatureC)。当 Info、Data 或 Entity 模糊了含义时,优先使用更具体的名称,同时在含义清晰时保留既有术语。
  • 对齐同级概念:在相关 API 中保持动词约定一致。
  • 不要使用编码实现历史而非当前含义的名称。像 Base、New、Old、Initial 这样的后缀需要有真正的语义区分,否则就删掉它们。
  • 避免为领域值使用晦涩的标识符(v、n、$n);短名称仅在小型的约定俗成的上下文中可以接受(循环索引 i、坐标 x/y、泛型参数 T/K/V)。

代码结构与组织

  • 优先复用而非重复。代码库中已有许多工具函数——善加利用。如果你引入了重复逻辑,请将其提取为共享工具。
  • 通用辅助函数不属于功能文件夹。将其提升为共享工具。
  • 保持函数小巧且职责单一。100 行以上的函数难以审查——拆分为专注的具名辅助函数,每个函数只承担一项职责,并具有最小的依赖面。必要时,用单元测试覆盖。
  • 将不同的复杂 JSX 提取为具名组件。根据复用性、耦合度、可测试性和可读性来选择同文件还是独立文件。

注释

  • 默认不写注释。命名良好的标识符已承载了 what。
  • 注释应当简洁。仅当 why 不明显时,添加一段简短、简洁的注释:一个变通方案、一个隐藏的不变量、一个微妙的顺序约束、一个巧妙的归约。绝不要记录实际实现,聚焦于意图和 why。
  • JSDoc 描述契约,而非实现。在导出的函数、hooks 和组件上,说明调用方会得到什么:它做什么、返回什么,以及在边缘情况下会发生什么(例如加载时返回 undefined)。

测试编写

测试应验证预期的用户可见行为或函数、组件的公共契约,而非其内部实现。保留该契约的重构不应破坏测试。

对于新增或修改的测试,包括 Jest 和 Cypress:

  1. 明确预期契约。 从需求出发:用户或调用方应当观察到什么?阅读相关源码以理解环境准备前提和执行路径,而不是把当前实现当作正确行为的定义。
  2. 搭建贴近真实的测试夹具。 提供达到被测行为所需的 props、state、模拟数据和 provider。模拟端点中的数据无法满足对组件 prop 的条件判断。
  3. 规划覆盖范围。 明确每个用例的前提条件、操作和可观测结果。对于复杂场景,一份简短的用例图有助于组织覆盖范围。
  4. 按照以下规则编写测试。
  5. 使用 Verify before done 审查已完成的改动,包括每个测试是否真正验证了它声称的行为。
  6. 总结结果。 简要说明新增的覆盖范围,以及任何未解决的问题或未运行的检查。遵循用户要求的回复格式。
  • 使用前端指南中的 setup pattern。 新增、移动或以其他方式编辑的参数化单元/组件 setup 函数应采用类型为 SetupOpts 的具名选项,而不是位置参数或布尔值。
  • 让 mock 与其真实契约保持类型一致。 优先导入函数并使用 jest.mocked 来 mock 导出。使用 jest.requireMock 时,仅当 mock 保留了该导出契约,才提供 typeof import("module-path")。为 jest.fn 回调提供有类型的实现,或提供与真实回调匹配的显式参数/返回类型;裸 jest.fn() 默认是 any。
  • 让夹具与拥有它的契约保持类型一致。 为模拟数据和延迟 promise 使用规范响应类型。从夹具本身推导类型并不能建立与 API 的兼容性。对解析或拦截的请求/响应体应用边界规则:在使用前将未类型化的结果捕获为 unknown,包括在断言中。将 unknown 值与独立的预期对象比较时不需要对其进行收窄。
  • 复用测试基础设施。 在添加 provider、服务 mock 或异步辅助工具之前,先搜索共享测试支持。对端点使用共享的 server-mock helpers,并在合适时使用 metabase/utils/promise 中的 defer<T>() 来控制异步完成。在扩展共享辅助工具时,不要破坏其他调用方。
  • 测试要真正验证其声称的内容。 通过重复操作来测试重复行为。在检查待处理行为或顺序时,要控制异步完成并在相关的中间状态进行断言,而不只是等一切结束后再断言。检查预期效果以及与契约相关的不期望效果的缺失。如果调用次数很重要,不仅要断言参数,还要断言调用次数;toHaveBeenCalledWith 允许存在额外调用。在测试重试之前,先等待失败变为可观测状态。
  • 断言可观测的结果。 检查渲染行为、返回值或契约要求的外部可观测效果。仅当回调参数、调用次数或顺序属于该契约的一部分时才对其进行断言,而不是为了镜像内部辅助函数调用或组件状态。复用适用的测试约定,而不是盲目照搬附近的断言。
  • 质疑断言。 如果预期的行为缺失或错误,这个测试会失败吗?对于多步交互,验证步骤之间可观测的转变——而不仅仅是最终状态。
  • 修复测试时保留其原本的覆盖意图。 修正含糊的选择器或有缺陷的 setup,但不要丢弃该测试原本要验证的行为。如果断言本身错误陈述了预期契约,请根据需求修正它;不要仅仅为了匹配当前实现而弱化它。

完成前验证

  • 完成后运行项目类型检查(参见上文共享的 TypeScript 命令)。
  • 审查已完成的更改。 使用 typescript-review 对照适用的准则检查完整 diff 和新文件,包括测试与辅助文件。测试、lint 和类型检查通过并不代表符合准则。

评论

支持 Markdown