ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Metabase 前端 TypeScript 编码规范实战:从 no-any 硬性红线、类型建模到可验证的交付闭环

2026/9/10 3:35:49 拓冰建站 浏览量
Metabase 前端 TypeScript 编码规范实战:从 no-any 硬性红线、类型建模到可验证的交付闭环 Metabase 前端 TypeScript 编码规范实战从 no-any 硬性红线、类型建模到可验证的交付闭环【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabaseTypeScript/JavaScript 是 Metabase 前端的核心语言仓库中沉淀了一套以「类型安全优先」为基调的编码规范并以.claude/skills/typescript-write/SKILL.md技能文档的形式固化下来供编码 Agent 与人工开发者共同遵守。本文围绕这份技能文档逐条拆解其背后的规则、配套命令与仓库内真实落地证据含 ESLint 规则实现、类型定义与 package.json 脚本帮助你写出符合 Metabase 标准、可通过自动化校验的 TS/TSX 代码。技能文档的定位Agent 与人类共用的编码准则.claude/skills/typescript-write/SKILL.md是仓库内 Claude Skills 目录中的一个技能定义其 YAML frontmatter 声明了用途name: typescript-writedescription: Write TypeScript and JavaScript code following Metabase coding standards and best practices. Use when developing or refactoring TypeScript/JavaScript code.也就是说该文档负责约束「在 Metabase 代码库中编写/重构 TS/JS」这一行为本身目标对象既包括调用技能的编码 Agent也包括遵循同一套准则的开发人员。它通过指令组合引用了三个共享知识文件均位于 .claude/skills/_shared/ 目录development-workflow.md——自主开发工作流不读写项目目录之外的文件、先写失败测试再修复、小步自治迭代、持续跑针对性测试与 lint、先理解既有模式再动手、不代提交 commit 交由用户审查、最小化注释。typescript-commands.md——lint / format / type-check / 测试的可用命令速查。react-redux-patterns.md——RTK Query、Redux、Hooks、加载与错误态等数据层与组件层模式。在技能目录中它并非孤立存在.claude/skills/下还有typescript-review评审视角、clojure-write、docs-write、e2e-test等配套技能共同构成「写 → 查 → 测 → 文档」的开发闭环。本文聚焦 write 侧的 TypeScript 规范本身。命令基线写代码时随时可跑的工具链技能要求开发过程中持续使用 lint 与类型检查命令定义在 .claude/skills/_shared/typescript-commands.md真实脚本位于仓库根目录 package.json阶段命令实际定义package.jsonLintbun run lint-eslint-pureeslint --cache --cache-strategy content --max-warnings 0 --report-unused-disable-directives enterprise/frontend frontend e2e第 476 行格式检查bun run lint-format-pureoxfmt --check {frontend,enterprise/frontend,e2e}/**/*.{js,jsx,ts,tsx,css}第 479 行格式化bun run formatoxfmt --write {frontend,enterprise/frontend,e2e}/**/*.{js,jsx,ts,tsx,css}第 485 行类型检查bun run type-check-pure./node_modules/typescript7/bin/tsc --noEmit第 508 行单测单文件bun run test-unit-keep-cljs path/to/file.unit.spec.jsjest --maxWorkers4第 498 行单测按 patternbun run test-unit-keep-cljs -t pattern同上由 Jest 的-t过滤用例ClojureScript 测试bun run test-cljsbun install shadow-cljs compile test node target/node-tests.js第 491 行几个值得注意的实现细节统一使用 Bun 而非 npm/yarnpreinstall脚本第 483 行会检查npm_execpath中是否包含bun否则直接报错退出。因此文档中所有脚本都写成bun run ...。type-check-pure调用的tsc来自名为typescript7的独立依赖./node_modules/typescript7/bin/tsc --noEmit即仓库内置了定制/分叉版本的编译器完整的type-check第 507 行则先clean:cljs build:cljs再做同样的type-check-pure。技能里「完成 TS/TSX 修改前必须跑bun run type-check-pure」即针对这种无需先构建 CLJS的纯前端校验路径。lint-eslint-pure带--max-warnings 0 --report-unused-disable-directives意味着警告即失败、无效的 eslint-disable 也会报错属于零容忍配置。Noany不可妥协的硬性红线技能开篇即把「禁止any」定义为 hard rule并给出了三种形态的禁止范围不允许显式或隐式any包括any注解、as any/as unknown as、被推断为any的无类型参数/返回值、隐式any的解构与数组/对象字面量。未类型化的第三方/边界值必须在边界处定型用声明的类型、unknown 类型守卫type guard或一个小的类型化包装绝不允许any向业务代码内部渗透。每次 TS/TSX 变更完成前必须执行强制类型校验前述type-check-pure若环境可用 TypeScript LSP还需对变更符号做 hover 与 go-to-definition 检查。这条红线的存在意义在于Metabase 前端体量巨大any一旦穿透边界就会切断整个类型图的连通性。因此在 review 场景见配套的 typescript-review/SKILL.md中「是否引入新的any」通常是第一检查项。类型收紧能修签名就别写强转「Type tightening」章节的核心哲学是出现类型问题时优先修正函数签名而不是用断言绕过去。逐条展开如下避免类型断言与松散的unknown——修复签名本身。很多时候断言是「签名错了」的信号。函数只用到宽对象里的一个字段就只接收那个字段。把入参从WholeObject收窄为WholeObject[field]后调用处的 cast 常常自然消失。在写 cast 之前先考虑PartialT、PickT, K、RecordK, V与泛型。它们用类型系统表达意图而非用断言压制类型系统。值原样流经组件且调用方已知类型时优先把 props/组件做成泛型T由调用方提供精确类型。宁用unknown也不要用松散类型在使用点收窄——unknown强制你写出守卫。对象字面量用satisfies当配置对象、查找表、可辨识字面量需要在「不拓宽类型」的前提下满足某个类型时satisfies优于: T会拓宽也优于as T不安全。避免非空断言!优先用守卫、提前 return 或?.只有在「非空性可证明成立且作用域局部化」时才允许!且必须配注释。不做冗余运行时强转已类型化的值不要再包Number()/String()/Boolean()。类型守卫统一放在frontend/src/metabase-types/guards/不允许在局部重复定义——这是「复用优于复制」在类型层上的体现。关于最后一点仓库证据非常清晰frontend/src/metabase-types/guards/ 目录集中存放守卫例如 card.ts 中的isSavedCardcard is Card、dashboard.ts 中的isVirtualCard、parameters.ts 中的isDimensionTarget等均为标准 TS 自定义类型守卫x is T谓词形式。无法避免的 cast必须写真实的理由注释绕不开的 cast 需要一条真实理由注释。仓库用自定义 ESLint 规则强制这一点规则注册于 frontend/lint/eslint-plugin-metabase/index.js完整实现在 frontend/lint/eslint-plugin-metabase/rules/no-unjustified-type-casts.js对TSAsExpressionexpr as T与TSTypeAssertionTexpr两类节点做检查任何位于 cast 前的注释即可使其通过同时豁免as const断言与嵌套在最外层 cast 内部的 cast。该规则同时明确永远不要写// Unjustified type cast. FIXME这类遗留占位注释——它只存在于规则上线前就有的历史 cast 上照抄它等于让一个无理由的 cast 骗过 linter。如果你说不清 cast 为什么安全那说明这个 cast 是错的正确做法是修类型。从规则源码看第 30-41 行isConstAssertion与isOutermostCast两个分支会提前放行其余一律要求「注释在紧邻 cast 的前一行/同行含被 oxfmt 抬升到三元操作符?/:行尾的注释」才通过校验。类型建模复用领域类型、让数据契约保持窄而精确「Type modeling」章节解决的是「新类型从哪里来、边界怎么画」复用既有类型不重复声明。使用metabase-types/api提供的规范化 ID 与领域实体类型并以它们为键构造数据结构如new MapConcreteTableId, …()。不要在业务文件里复制生成的/API 类型应通过组合或派生获得Pick、Omit、索引访问SomeType[field]、ReturnType。仓库中的实际定义可印证这套命名体系例如 database.ts 的DatabaseId number、field.ts 的FieldId number、table.ts 的ConcreteTableId物理表与VirtualTableId如card__17这类虚拟表 ID合成TableId以及SchemaId/SchemaName等。善用泛型让 TS 自动推断正确类型对需要「可复用且类型安全」的函数与组件不要畏惧引入较复杂的泛型只要它们能取代手工收窄。按真实数据契约建模保持类型窄键可能缺席用field?: T键恒存在但值可能是undefined用field: T | undefinedAPI 显式返回 null 才用| null。领域联合类型优先于宽泛的string/number/松散Record。定义/修正类型时要参照 API 实现先到 Clojure 侧找到对应 endpoint 实现确认字段的真实形状与可空性而不是凭前端臆测。这呼应了本文后面「null 与 undefined」中「对照 API 实现核对可空性」的原则。可变状态用可辨识联合discriminated union 穷尽检查。把「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仓库确实把 ts-pattern 作为一等依赖它在 package.json 中被声明为ts-pattern: ^5.9.0。从常量推导联合类型用as consttypeof/keyof让「类型」与「值」不可能漂移。readonly/ 不可变性对无意变更的输入组件 props、共享常量、导出配置优先使用readonly T[]/ReadonlyArrayT。显式建模异步与错误状态loading / error / empty 不允许「隐式存在」要用可辨识联合或数据层自带的类型化结果表达。Null 与 Undefined源头收窄消费点兜底关于空值处理的四条准则强调「在哪一层解决」源头收窄如果某值只在极端角落才可选不要在每一层都把它当undefined传递——在生产数据的源头producer加守卫。给可选值合理默认消费点用?.与??兜底。列表先过滤再使用不要带着可能为 null/undefined 的成员进入map或其他迭代。避免非严格空比较X ! null只有在X确实可能为null时才有意义否则用严格比较或直接收窄类型必要时使用checkNotNull这类工具。对照 API 实现核对可空性去读对应 API endpoint 的 Clojure 实现确认字段到底能不能为 null——而不是想当然地到处加?.。这与「类型建模」中对?/| undefined/| null的三种区分是同一套世界观的两个面一端管「字段是否存在」另一端管「值是否为空」都需要以后端数据契约为准。命名描述实体而非机制或历史名字描述值承载的实体而不是实现机制。对齐同类概念同一组相关 API 之间保持动词习惯一致例如增删改查的命名模式。不要用编码实现历史命名Base、New、Old、Initial这类后缀必须存在真实的语义差异否则删掉。避免晦涩标识符领域值不要叫v、n、$n短名只允许出现在公认的极小上下文里循环下标i、坐标x/y、泛型参数T/K/V。代码结构与组织复用优先函数小而专复用优于复制仓库已有大量工具函数优先使用若发现自己重复实现了相同逻辑就抽取为共享工具。通用 helper 不放功能目录泛用工具要上提到共享层避免在 feature 目录里私藏可复用逻辑。函数保持短小、单一职责超过百行的函数评审成本高应拆成多个聚焦的具名 helper各司其职、依赖面最小必要时补单测。把复杂 JSX 抽成具名组件依据复用度、耦合度、可测性与可读性决定抽取到同文件还是独立文件。这部分与共享文件 react-redux-patterns.md 中的组件组织原则展示组件配 Storybook stories、容器组件向下传窄 props、深层 state 尽量在高层读取等互相呼应共同定义了 Metabase 前端的代码形态。注释默认不写写就写 why默认不写注释命名良好的标识符已经承担了what代码做什么的表达。注释必须精简且只解释whyworkaround、隐藏不变量、微妙的顺序约束、巧妙的归约才值得注释。永远不要复述实现过程注释聚焦意图与原因。这与 development-workflow.md 中的要求一致「只记录代码本身无法传达的非显然意图、权衡或约束不叙述代码做了什么让代码靠命名与结构自解释」。交付前校验把规范变成可执行的检查技能的收尾章节「Verify before done」只有一句但很关键完成后运行项目类型检查即共享命令里的bun run type-check-pure。结合整个技能体系一次完整的 TS 变更交付流程可以归纳为先补失败测试development-workflowAdd failing tests first, then fix them用bun run test-unit-keep-cljs file或-t pattern跑针对性用例开发中持续跑bun run lint-eslint-pure与bun run type-check-pure让no-any、no-unjustified-type-casts等规则尽早暴露问题结束时再做bun run lint-format-pure/bun run format统一格式不代提交 commit把改动留给用户审查development-workflow 明确要求。总结一套「可被自动化强制」的类型文化纵观 SKILL.md 全文它的核心并不只是罗列「风格偏好」而是把类型安全诉求转译成了可被工具强制执行的检查项no-any靠心智与 code review 把关no-unjustified-type-casts靠仓库自带 ESLint 规则兜底.exhaustive()靠编译器穷尽type-check-pure靠tsc做最终闸门。对于在 Metabase 前端frontend/与enterprise/frontend/写 TS/TSX 的开发者与 Agent 而言这套规范的实际效果是类型图保持连通、边界可审计、重构可放心进行。若需要从评审侧进一步验证这些约定是否被遵守可直接参考同目录的 typescript-review/SKILL.md 与仓库自定义 lint 规则集 frontend/lint/eslint-plugin-metabase/。【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考