资讯动态

t3code:一套类型安全、可测试、可追踪的工程编码规范

发布时间:2026/10/26 8:45:52 来源:尧图企业网站定制
t3code这个代号是我给自己内部一套编码规范起的名字。如果你接手过那种改一个接口字段要全局搜三遍、上线即出线上事故、日志打了一堆排查还是两眼一抹黑的项目那你大概能理解我为什么要做这件事。简单说t3code是我的工程实践中沉淀出来的一套编码准则与落地工具链核心只有三条铁律类型安全Type可测试Test可追踪Trace。它不挑框架、不绑定语言适合所有想把工程质量往上拉一个台阶的团队尤其适合那些项目已经长到能跑就行阶段、开始频繁出幺蛾子的团队。这篇文章会把 t3code 的来龙去脉、设计逻辑、落地步骤和踩坑记录完整写出来你可以直接参照着在自己项目里做一次体检和改造。1. 我为什么要搞 t3code背景与动机1.1 一次让我下定决心的大事故事情发生在两年前。我们维护的一个 ToB 后台系统某个夜间任务突然大批量报错所有涉及订单状态流转的请求全部超时。值班同事顺着日志一查发现核心问题是一个非常离谱的类型错误上游服务把一个金额字段从number改成了string下游代码里有一个total / 100的运算字符串除以数字在 JavaScript 里不会直接抛异常而是会得到一个NaN然后这个NaN被写进数据库后续所有依赖这个字段做计算的逻辑全线崩盘。真正让我冒冷汗的不是报错本身而是整个排查过程报错信息散落在六个服务的日志文件里格式各不相同有的打印了完整调用链有的只有一行孤零零的undefined。我们花了差不多四十分钟才把链路拼出来然后发现那段出问题的代码根本没有测试覆盖而且因为接口字段没有类型约束上游在改协议的时候我们完全无感。那次事故之后我做了个统计过去三个月本组一共出了 17 个线上问题其中 10 个是纯低级类型错误、4 个是改动后回归测试没覆盖、3 个是排查链路太长导致恢复时间超时。这就是 t3code 的起点我不是为了追求某种最佳实践的虚名而是想用一套确定的规则系统性地干掉这三类问题。1.2 t3code 的三条铁律Type、Test、Trace我最初其实想叫它 TSCType Safe Code但缩写撞车了后来干脆叫 t3code三个 t 取了三个词的首字母Type类型安全。所有跨模块、跨服务的数据交换必须有明确的类型定义编译期能拦住的错误绝不留到运行期。Test可测试性。每个关键业务模块必须有对应的单元测试和集成测试改动时回归测试是底线。Trace可追踪性。日志必须自带上下文和链路标识任何一次线上异常都能在五分钟内定位到具体代码路径。之所以把这三条放一起而不是只做其中一条是因为它们互相咬合类型约束让数据流变得可预期测试把预期固化成回归基线追踪则在预期被打破时给你最快的定位路径。缺了任何一条另外两条的效果都会打折。2. 为什么是这三条选型逻辑与价值拆解2.1 类型安全把运行期错误掐死在编译期很多从 JavaScript 转 TypeScript 的团队会有一种错觉加了 TS 就等于类型安全了。实际上我见过的项目里七成都还停留在TS 皮、JS 心的阶段——any满天飞接口返回的类型全靠我记得来写改个字段要全局搜索才知道影响范围。t3code 对 Type 的定义不是用了 TS而是三个硬性指标第一严格模式全开。strict必须是truenoImplicitAny、strictNullChecks、noUncheckedIndexedAccess一个都不能少。很多人怕开严格模式之后编译错误太多但我的经验是那些报错不是负担是系统在告诉你哪里藏着隐患。第二跨模块边界的数据必须有共享类型。前端调接口、后端返回数据两边的类型定义必须来自同一个来源可以是swagger-codegen自动生成的 SDK也可以是放在独立包里的类型定义总之不能出现我看一眼返回值手写一个 interface的情况。第三禁止any的逃生通道。我理解老项目有历史债所以 t3code 给的替代方案是unknown 类型守卫。unknown 逼着你先把数据校验一遍再使用本质上就是把运行期的隐患提前到边界处解决。这三点里最难执行的是第三点因为它要求每个团队成员都养成先缩小类型再操作的习惯。2.2 可测试性为可维护性做的最划算投资说到测试很多人的第一反应是写测试时间太长了影响进度。但我的算法不一样写一个用例可能花二十分钟但它能保证你下一次改这个模块的时候不用花四十分钟来提心吊胆地验证。t3code 的 Test 原则不是尽量多写而是在正确的地方写、写正确的测试。我落地的时候用的是经典测试金字塔的变体单元测试覆盖纯函数、工具函数、状态机、复杂计算逻辑。这一层跑得快、写起来心智负担低占比最多。集成测试覆盖 DB 访问、外部服务调用、消息队列等带 IO 的模块。这一层要用真实的测试库或容器化的依赖不能全 mock否则测了跟没测一样。端到端测试只覆盖核心用户链路比如登录、下单、支付。数量少但每一个都价值极高。t3code 里有一个强制要求核心业务函数的测试覆盖率必须达到 100%注意是函数覆盖率不是行覆盖率。行覆盖率看着好看但可能整个函数都没被调用过、只是代码路过了。这个指标只针对核心模块至于那些一次性脚本和表层 UI 逻辑我会建议用更轻量的方式对待。2.3 可追踪性线上排查不再靠猜过去我们排查线上问题的常规流程是这样的先找到报错时间点然后去各个服务的日志文件里 grep运气好能拼出上下文运气不好就只能开在线调试一遍一遍加日志、发版、复现。这个流程效率极低因为日志之间没有关联。t3code 的 Trace 原则要解决的就是日志孤岛的问题。Trace 的实现我不建议自己造轮子直接用现成的链路追踪协议和日志结构化方案。核心是让每一行日志都携带三类信息traceId一次请求或一个任务的全局唯一 ID从入口处生成贯穿所有内部调用。spanId每一次内部服务调用或关键步骤的 ID可以理解为 trace 里的一个环节。业务上下文用户 ID、订单号、设备 ID 等等这些是关键检索字段。有了这些日志从散落的信息流变成了可索引的事件流任何一个报错都能通过 traceId 把整条调用链拉出来。我再强调一遍这个能力不是锦上添花是线上故障恢复时间从小时级降到分钟级的核心。3. 落地实操把现有项目改造成 t3code3.1 第一步从类型层开始动刀改造一个存量项目我强烈建议从类型层开始因为它是后续测试和追踪的基础。类型不清晰测试根本没法写类型不严谨链路追踪拿到的数据也未必准确。具体操作分四步走第一步开启 TypeScript 严格模式。直接改tsconfig.json里的strict: true然后面对那一堆编译报错。报错数量通常会让你崩溃但别慌给每个模块排优先级先改核心业务模块的边缘代码用// TODO标记后逐步清理。第二步消灭显式any。这一步没法靠人肉做效率太低我配置了 ESLint 的typescript-eslint/no-explicit-any规则让 CI 直接把带any的代码卡住。第三步整理接口类型来源。如果后端是 OpenAPI 规范那就接一个自动生成 SDK 的脚本如果是内部 RPC那就把 proto 文件当作唯一类型来源。这一步的核心是让类型定义自动同步而不是靠人工维护。第四步给外部数据加运行时校验。就算编译期类型再严格网络请求返回的数据还是有可能不符合预期我在所有 API 边界处接一个 zod schema用parse方法做运行时校验校验失败直接抛异常而不是带着脏数据往下走。这里有一个你迟早会踩到的坑第三方库的类型定义可能是坏的或者过时的硬用会让你很痛苦。解决方案是用types仓库里的修正版本或者用declare module做局部补丁不要因为一个库的类型不好就放弃整个严格策略。3.2 第二步搭建测试基座并补齐核心用例类型层搞定之后写测试会舒服很多因为函数的入参和返回值都有了明确约束。测试基座的搭建我一般用 Vitest 或 Jest 作为 runner配好coverage报告然后按模块裁剪测试计划。写用例的时候我有一套优先级排序第一优先级是纯函数和复杂逻辑。比如一个计算优惠券叠加金额的函数可以列十几个测试用例满减、折扣、叠加、封顶、过期、并发修改边界值。这类测试收益极高因为业务核心的错误往往都集中在这些看似简单的计算里。第二优先级是 IO 模块的集成测试。比如访问数据库的仓储层很多团队会把这个测试 mock 掉数据库我反而建议用真实的测试库每次跑之前初始化 schema跑完插入的数据回滚慢一点但真实可靠。第三优先级是核心链路的端到端测试。选两三条最核心的用户动线用 Playwright 或 Cypress 跑起来每晚定时执行。这类测试不需要多但绝对不能缺。我一直提醒团队一句话测试的意义不是数量而是你敢不敢在改完代码之后顺畅地跑一遍并确信没改坏东西。如果你发现自己整个下午都在维护测试到让覆盖率数字好看的状态那就说明测试策略本身有问题。3.3 第三步接入结构化日志与链路追踪Trace 层的落地需要根据你的服务架构选方案。我团队内部用的是统一日志中间件把日志打到 JSON 格式并且强制每个服务初始化的时候从请求头或者消息队列消息体里提取traceId。如果项目还在单体阶段Trace 的落地可以很轻写一个 logger 封装进程启动时设置一个traceId所有日志自动输出这个 ID请求结束时清掉。如果已经是微服务架构那就正经引入 OpenTelemetry让各服务之间通过 HTTP header 或者消息系统传递上下文。不管哪种方式核心目的是让日志之间能串成一条线。日志格式我强烈建议统一成 JSON。很多人习惯看纯文本日志觉得可读性好但纯文本没法被检索工具高效处理排查问题时你往往会后悔没早改 JSON。JSON 日志的字段至少要有time、level、message、traceId、spanId、userId、orderId、duration。这里有一个容易被忽略的细节日志里不要记录敏感信息尤其是密码、身份证号、手机号。之前有个同事为了排查方便把整个请求对象原样打进日志结果里面带了用户的明文密码这事儿要是出了合规问题比线上 Bug 严重得多。4. 常见问题与排查技巧实录4.1 类型层我的 TypeScript 严格模式踩坑日记开启严格模式之后我最常遇到的问题之一是strictNullChecks带来的连锁反应一个函数内部原本感觉不会为空的变量编译器非要你走空值判断。起初会觉得烦但我后来养成了一个习惯把这个警告当作逻辑漏洞的提示逐个检查为什么这里可能为空。另一个高频问题是noUncheckedIndexedAccess读取数组元素时返回值会带上undefined。这会逼你在访问数组元素前做存在性判断一开始代码会显得冗长但它确实拦住了一次真实事故一个小伙伴读了下标 3 的元素而数组长度只有 3以前在 JS 里会静默变成 undefined然后一路传给后续函数导致 NaN 落库。如果你也遇到跟this相关的类型问题别想着用any绕过。把方法改成普通函数、显式传入依赖这个模式在团队里推行之后副作用函数的可测性也变好了是顺便带来的红利。4.2 测试层覆盖率 100% 了为什么 Bug 还是漏网有一段时间我们的测试覆盖率到了 92%但线上还是漏了一个严重的边界错误。复盘时发现那个出问题的函数是一个典型的条件分支密集函数测试虽然把所有分支都跑到了但因为每个分支都是输入建号、断言结果的固定模式根本没验证状态之间的转换过程。从那以后我给测试评审加了一条看测试不能只看覆盖率报告还要看有没有状态转换测试和异常路径测试。比如一个状态机模块只测正常流转是不够的还必须测重复调用、异常跳转、非法输入等场景。还有一次有同事写单元测试的时候把所有数据库依赖都 mock 掉了结果某天底层 ORM 升级查询语法改了单元测试全绿一上线上直接报错。原因是 mock 行为模拟的是你想象中的数据库行为不是真实的数据库行为。所以集成测试里要尽量用真实的依赖和容器化环境单测才适合用 mock。4.3 追踪层日志打了 traceId 还是查不到链路原因有三个这种情况在推广 Trace 的时候经常出现我总结出最常见的三个原因。第一个原因是入口服务没有生成traceId后续服务都是孤儿日志。排查方法是查看最开始的请求日志里有没有traceId字段没有就去检查中间件初始化的顺序。第二个原因是跨服务调用时忘了把traceId放到请求头里传递。HTTP 服务之间好办写上request-id中间件就行但 MQ 消息很容易漏因为生产者取不到下游消费端的上下文。这个必须在生产者发送消息之前把traceId写进消息体消费者第一件事就是读取它。第三个原因是没有统一日志框架各个服务打的日志一个是 JSON、一个是纯文本检索的时候对不上号。解决的办法就是统一 SDK从日志输出到上下文管理全都用同一套谁自定义格式就 code review 打回去。5. 关于 t3code 我的个人体会折腾 t3code 这一年多我最深的感受是这不是一个技术方案更像是一套工程纪律。技术选型其实都现成TS 严格模式、测试框架、OpenTelemetry都是成熟工具真正的难点在于让每个成员都理解为什么必须这么做并且在进度压力和工程纪律冲突的时候能守住底线。如果你也想在自己团队推行类似的东西我的建议是从最小的切口进去先挑一个最近事故最多的模块做类型补全加核心用例再加一条链路追踪日志单独量化这个模块的状态恢复时间和回归 Bug 数量。有了数据推广阻力会小很多。最后再分享一个小技巧t3code 它不是一成不变的每个季度我都会把团队踩过的坑按 Type / Test / Trace 三个维度回填进规范文档让它跟着项目一起长。现在回看这套规范帮我省下的不只是修 Bug 的时间更是那种深夜被手机震醒、却不知道问题在哪里的焦虑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑