从代码腐化到 t3code:TypeScript 工程化编码规范实践

发布时间:2026/10/27 23:02:08

从代码腐化到 t3code:TypeScript 工程化编码规范实践 先说个背景。年初我接手了一个跑了快两年的 TypeScript 全栈项目代码量四万多行看起来一片繁荣。真正开始迭代才发现处处都是any、隐式类型、三四种命名风格共存一个组件里塞了两千行业务逻辑。最离谱的是同一个数据获取逻辑在不同页面里写了五遍每一遍还微调得不一样。代码评审时大家不是在卷逻辑而是在争论“你为什么要用这种写法”。那时候我就意识到这个项目缺的不是技术是一个统一的编码标准。后来我花了差不多三周时间把我们踩过的坑、评审时吵过的架、重构时拆过的雷全部梳理了一遍定了一套规范项目代号就叫t3code。t3 不是版本号它是三个 TType-first类型优先、Test-driven测试驱动、Team-friendly团队友好。这篇文章就把 t3code 从思路到落地完整拆一遍包括技术选型、目录结构、核心编码规则、完整实操流程和踩坑记录适合正在做 TypeScript 中大型项目的团队参考也适合个人开发者想提前建立良好工程习惯时抄作业。1. 为什么需要 t3code三个 T 是怎么来的1.1 先从一次典型事故说起有一次我们上线了一个营销活动页面运行半小时后用户集中反馈“页面白屏”。查日志发现是接口返回数据的某个字段从string变成了null而代码里直接对这个字段调用了.split()。老代码里这个字段从来没空过所以没人处理。生产故障修了半小时本质原因就一句话没有类型约束就没有人知道这个字段可能为空。这其实是所有动态语言和弱类型习惯带进来的老毛病。JavaScript 项目跑得越久运行时错误越难追溯因为错误不是在你写错的那一行暴露的而是在用户使用到那条路径时才爆发。TypeScript 的价值不在于“用上了”三个字而在于把边界、空值、联合类型这些信息在编译期就暴露出来。t3code 的第一条原则 Type-first就是从这个教训里长出来的。1.2 linter 管不住“风格病”你可能觉得不是有 ESLint 和 Prettier 吗它们确实能帮我们把分号、引号、缩进统一掉但统一不了更深层的风格状态该放哪儿、组件该怎么拆、副作用怎么收敛、接口返回怎么处理。这些是 linter 的盲区却恰恰是代码腐化的开始。t3code 做的事情是在 lint 之上补一层约定层。它不只告诉团队“不能用any”还告诉团队“这个场景下你应该怎么写、为什么这么写、写完之后长什么样”。三层结构大概是Lint 层ESLint 预设规则加严格 TS 规则自动钳制。约定层t3code 的编码规范人肉遵守 评审检查。模板层项目骨架、目录结构、脚手架让正确做法成为默认做法。1.3 三个 T 回答了三个问题Type-first回答的是“这个数据的形状是什么”接口入参出参、组件 props、全局状态一律先定义类型再写实现。Test-driven回答的是“凭什么相信这段逻辑没问题”核心逻辑先写测试测试不是附属品是设计工具。Team-friendly回答的是“别人接手时能不能看懂”命名语义化、组件单一职责、数据流收敛让代码自己会说话。这三个词听起来像是老生常谈但真正把它们拆成可检查的条条框框再去落地你会发现很多项目连第一条都没做到。T3code 就是把这三点做成了 checklist。2. 项目骨架与技术选型让正确写法成为默认2.1 技术栈选择逻辑t3code 不是一个框架但为了方便落地我基于当时团队最熟悉的生态搭了一套参考底座。选型逻辑只有一句话选那些能天然约束代码行为的工具而不是需要靠自觉的工具。前后端我用的是 Next.js 14App Router TypeScript 严格模式 Prisma PostgreSQL。有人可能会问为什么不用别的数据库或 ORM因为 t3code 的核心诉求是“类型安全贯穿全链路”。Prisma 能从数据库表结构直接生成类型这个特性让 Type-first 在数据层落地变得几乎零成本。前端调用接口时如果用的 tRPC连手写 API 类型都不用类型直接从服务端函数推导到客户端。这条链路对 TypeScript 项目来说是目前我想得到的“类型穿透力”最强的组合。如果你项目里暂时不能换技术栈也没关系。t3code 的规范是技术栈无关的你可以继续用 REST Axios 手写类型规范照常适用。2.2 目录结构按模块聚合不按技术分层传统项目喜欢按技术分层components/、hooks/、utils/、services/各一大文件夹。代码少的时候看着清爽代码量上来之后就完蛋了一个业务功能涉及的文件分散在四个目录里改一个需求要跨五个文件夹。t3code 推荐的是按功能模块聚合。一个完整的业务模块自带它的组件、逻辑、类型、测试和路由目录长这样src/ modules/ auth/ api.ts # 接口调用与类型映射 auth.type.ts # 数据模型类型 components/ # 模块专属组件 hooks.ts # 模块逻辑 auth.test.ts # 单元测试 todo/ api.ts todo.type.ts components/ hooks.ts todo.test.ts shared/ # 跨模块共享的原子组件/工具 config/ # 环境与全局配置 app/ # 路由与布局这个结构的好处是改动代码时所有相关文件都在附近。删一个模块时直接删一个文件夹不会留下孤儿文件。共享代码会被谨慎对待因为放shared/是需要评审的避免“共享区”变成一个垃圾桶。2.3 TypeScript 严格配置第一步就要关掉的坑很多项目的 tsconfig 是从模板抄来的strict没开或者开了但配了一堆豁免。t3code 的底线是strict: true并且明确禁止下面这几项豁免{ compilerOptions: { strict: true, noImplicitAny: true, strictNullChecks: true, noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true, noFallthroughCasesInSwitch: true, noUnusedLocals: true, noUnusedParameters: true } }noUncheckedIndexedAccess是很多人不知道的好东西。开着它之后访问数组元素和对象索引的返回值类型会带上undefined这逼着你处理越界和缺键的情况。刚开始会很不习惯觉得代码啰嗦但跑一段时间你会发现线上 cannot read properties of undefined 这类错误会显著减少。exactOptionalPropertyTypes更严格可选属性一旦传进来就不能显式传undefined。这个选项能消除一类很隐蔽的类型漂移问题也让接口的契约感更强。3. 核心规则解析命名、组件、数据流、错误处理3.1 命名规范语义先于缩写命名问题是评审里最容易吵起来的。t3code 定了几条硬规矩组件文件名一律用PascalCase如TodoCard.tsx普通工具文件用camelCase。类型名用 PascalCase接口用I前缀不不用前缀。TS 的接口和类型别名在语义上没有区别加I前缀是没意义的。布尔变量用is、has、can开头isLoading、hasError、canSubmit。看一眼名字就知道数据类型。函数名用动词 名词fetchTodoList比getData好handleSubmit比submit在回调场景里更能表明身份。避免data、result、list这种无名氏变量至少得是todoListData、createResult。范围越大名字要越长。有一条特别的建议不要用any作为类型。如果暂时不知道类型用unknown然后做收窄。any等于把类型系统一票否决而unknown逼着你在使用前先证明它是什么哪怕改起来麻烦一点。麻烦是好事的开始。3.2 组件设计单一且薄组件这块是重灾区。t3code 给组件定义了三个特征薄、单、纯。薄组件文件原则上不超过 250 行。超过就拆。一个组件既管表格渲染又管弹窗编辑又管权限判断这不是组件是意大利面。单一个组件只做一件事。渲染列表的只渲染列表处理编辑弹窗的只处理弹窗。纯组件内部尽量不直接发请求、不直接读写 localStorage。这些副作用一律放进hooks里组件只负责“拿数据 渲染 UI 回调通知”。测试的时候mock hooks 比 mock 一堆 DOM 事件简单得多。举个例子一个列表页由三层组成// TodoListPage.tsx —— 页面层组合下面两层 function TodoListPage() { const { todoList, isLoading, toggleTodo } useTodoList(); return TodoListView list{todoList} loading{isLoading} onToggle{toggleTodo} /; }// TodoListView.tsx —— 纯展示层不感知数据来源 function TodoListView({ list, loading, onToggle }: TodoListViewProps) { if (loading) return Skeleton /; return ( ul {list.map((todo) ( TodoItem key{todo.id} todo{todo} onToggle{onToggle} / ))} /ul ); }// useTodoList.ts —— 逻辑层数据获取与管理 function useTodoList() { const [todoList, setTodoList] useStateTodoItem[]([]); const [isLoading, setIsLoading] useState(true); // fetch toggle 逻辑... return { todoList, isLoading, toggleTodo }; }页面组件几乎不写业务逻辑数据逻辑不掺 UI 渲染。改 UI 不动逻辑改逻辑不碰 UI这才是组件化的意义。3.3 数据流状态不要满天飞团队协作中很容易出现一种病props一层层往下钻钻到第五层才被用到中间层组件被迫带上没意义的props或者状态哪个组件想用就useState一份不同组件各存各的副本数据一不同步就开始互相甩锅。t3code 对状态管理有几条判断准则状态放在离使用者最近的公共祖先。如果只是父子组件共享放在父组件如果多个页面共享放进全局 store如果是服务端数据优先用请求层缓存React Query / SWR而不是自己维护副本。派生状态不要重复存。比如filteredList是list和filter的派生值直接用函数计算不要新增 state 去同步。存了两份就总有不同步的一天。不可变更新是底线。state.todos.push(newTodo)这种写法直接判负。更新状态一律用新的数组/对象引用这不仅是 React 渲染机制的需要也是可调试性的保障。数据流规整之后团队里最常出现的“这个数据怎么变了”这类问题会少一半以上。3.4 错误处理不吞异常不裸抛项目中错误处理的混乱程度远超想象。常见的烂片情节是try/catch捕获异常后console.error一下就当无事发生或者接口返回{ code: 1, msg: fail }前端拿到之后不判断code直接渲染页面就给你看原始 JSON。t3code 规定了几点不吞异常。catch块里至少要做一个用户可见的反馈哪怕只是 toast纯静默处理的 catch 必须在代码评审时被质疑。不确定的异常类型要收窄。catch (e)拿到的不是Error的保证。使用前先做判断e instanceof Error否则String(e)。统一错误处理出口。数据请求层把接口错误转换为业务错误码UI 层只面向错误码做展示不层层嵌套 try/catch。错误处理这块在我做评审 checklist 的时候是重点加分项。它不是功能逻辑但决定了一个系统在异常时刻的表现是稳定还是崩盘。4. 实操过程拿“待办事项”模块走一遍完整流程光说规则没用我拿一个真实模块完整走一遍 t3code 的标准流程。这个模块就做一件事拉取待办列表、勾选完成、删除待办。4.1 类型先行先写 schemat3code 里开始写任何实现前最先落地的文件是todo.type.ts。这一步相当于给整个模块画图纸。// todo.type.ts export type TodoStatus active | completed; export interface TodoItem { id: string; title: string; status: TodoStatus; priority: low | medium | high; createdAt: string; updatedAt: string; } export interface TodoListParams { page: number; pageSize: number; status?: TodoStatus; } export type CreateTodoInput PickTodoItem, title | priority; export type UpdateTodoInput PickTodoItem, id PartialPickTodoItem, status | priority | title;注意几个细节status没有用string而是用了联合类型active | completed这个字段的可选值就锁死了CreateTodoInput用Pick表达了“创建时只能传这两项”UpdateTodoInput用Partial表达了“更新时每一项都可选”。这些信息写出来后端接口的契约也顺便清楚了。4.2 测试先行先写测试类型定了写实现之前先写一个核心逻辑的测试。这里选一个足够复杂的逻辑对待办列表做优先级排序并分组。// todo.test.ts import { describe, expect, it } from vitest; import { groupTodoByPriority, sortTodos } from ./todo.utils; const mockTodos: TodoItem[] [ { id: 1, title: a, status: active, priority: low, createdAt: , updatedAt: }, { id: 2, title: b, status: active, priority: high, createdAt: , updatedAt: }, { id: 3, title: c, status: completed, priority: medium, createdAt: , updatedAt: }, ]; describe(sortTodos, () { it(should sort by priority: high medium low, () { const sorted sortTodos(mockTodos); expect(sorted[0].id).toBe(2); expect(sorted[1].id).toBe(3); expect(sorted[2].id).toBe(1); }); }); describe(groupTodoByPriority, () { it(should group active todos by priority, () { const grouped groupTodoByPriority(mockTodos); expect(grouped.high).toHaveLength(1); expect(grouped.low).toHaveLength(1); expect(grouped.medium).toHaveLength(0); }); });先写测试的好处是你会被迫在写实现之前考虑清楚“输入是什么、输出是什么、边界怎么处理”。测试跑红了然后去写实现直到跑绿。这个过程看起来多花了一点时间省下来的却是未来重构时手动点页面的时间。4.3 分层实现从工具函数到 Hooks 再到组件测试定义好行为后实现按三层来工具函数层是最纯粹的逻辑直接实现排序和分组。// todo.utils.ts const priorityRank: RecordTodoItem[priority], number { high: 3, medium: 2, low: 1 }; export function sortTodos(todos: TodoItem[]): TodoItem[] { return [...todos].sort((a, b) priorityRank[b.priority] - priorityRank[a.priority]); } export function groupTodoByPriority(todos: TodoItem[]): RecordTodoItem[priority], TodoItem[] { return todos.reduce((acc, todo) { if (todo.status completed) return acc; acc[todo.priority].push(todo); return acc; }, { high: [], medium: [], low: [] }); }逻辑层是一个自定义 Hook负责请求、状态、回调。// useTodoList.ts export function useTodoList() { const [todoList, setTodoList] useStateTodoItem[]([]); const [isLoading, setIsLoading] useState(true); const fetchTodoList useCallback(async () { try { setIsLoading(true); const data await fetchTodoListApi(); setTodoList(sortTodos(data)); } catch (error) { toast.error(待办加载失败); } finally { setIsLoading(false); } }, []); const toggleTodo useCallback(async (id: string) { const target todoList.find((item) item.id id); if (!target) return; const nextStatus: TodoStatus target.status active ? completed : active; await updateTodoApi(id, { status: nextStatus }); setTodoList((prev) prev.map((todo) (todo.id id ? { ...todo, status: nextStatus } : todo))); }, [todoList]); useEffect(() { fetchTodoList(); }, [fetchTodoList]); return { todoList, isLoading, toggleTodo, refetch: fetchTodoList }; }注意这个 hook 里没有直接碰 DOM没有直接渲染所有副作用都被封装起来组件拿到的是一组稳定的数据和回调。展示层组件的核心就是消费 hook 返回的数据做渲染。这部分相对最简单也是 t3code 觉得越简单越好的地方。4.4 自动检查与评审 Checklist模块写完后要过三层检查格式化与 Linteslint . --fix和prettier --write .项目里直接内置规则不给人纠结分号到底打不打的机会。类型检查tsc --noEmit确保全项目类型干净。代码评审按 t3code 的 checklist 逐条过。评审 pressure 最大的是这几条有没有任何类型的any漏网有没有组件超过 250 行有没有 catch 块是空手道吞异常的有没有状态存在本可以计算出来的重复项Props 有没有出现“透传黑洞”组件传了 8 个 props 只为了再传给子组件将这套 check 固化到 CI 的pnpm lint pnpm type-check pnpm test里以后评审的重点可以从“风格争吵”转回“业务正确性”团队氛围都改善了不少。5. 实践中的常见问题与避坑实录5.1 类型太深反而拖慢开发Type-first 推到一个极端会出现一个反噬类型定义一个文件写两百行一个通用类型套三重泛型改动一个业务字段要在十个文件里同步修改。这种过度设计会让团队悄悄绕开类型系统开始出现as unknown as Foo这种妖孽写法。我的建议是类型要服务于数据边界而不是服务于炫技。有实际意义的联合类型、满足业务边界的Pick/Omit、保证空值安全的收窄这是值得写的。嵌套十层的工具类型、为了消除一个报错硬写的映射类型不写也罢。代码评审时如果看到“读不懂的类型”我会直接要求作者讲清楚它的业务含义讲不清楚就拆掉。5.2 组件抽象过度为复用而复用有一次我们为了省代码把按钮、弹窗、下拉框的具体业务样式全抽到了一个BaseForm组件里结果人人都在传巨大无比的 config 对象组件内部的判断分支十几条改动一个模块的需求时天天担心影响另外五个模块。后来把BaseForm拆回各个业务模块自己的组件虽然每个模块多写了一百行但再也没有那种牵一发动全身的恐惧。t3code 对组件抽象的态度是只抽被三个以上场景验证过的复用不抽为了少写五行代码的复用。第一次需要直接复制代码第二次你可以考虑抽第三次再抽那才是成熟的时机。5.3 测试写着写着变成了实现副本有的测试写出来之后如果改了实现测试就必挂但测试本身没断言几件业务该做的事。这种测试我把它叫“实现复读机”。比如组件里调了 setState 三次测试就断言 setState 被调了三次超时时间从 1000ms 改成 2000ms测试也跟着改。这类测试没有守护业务只会拖累重构。正确姿势是测试行为而非实现。断言“点击按钮后待办列表出现了新的项”而不是断言“点击按钮后调用了某个函数两次”。行为不变重构随便改测试依然绿这才是测试真正该有的价值。5.4 团队里有人不遵守规范怎么办这是最现实的问题光有文档没有执行力规范等于废纸。我的经验是先把工具钳制做到位rules 直接写死在 ESLint 配置里不给你单文件注释豁免的机会。然后新项目或者新模块必须从模板脚手架生成不手动从零搭让目录结构和默认配置从一开始就符合规范。最后评审意见只对事不对人拿着 checklist 说话不搞个人审美。还有一个容易被忽略的点渐进式落地。老项目上万行存量代码不可能一次性全改完。t3code 的做法是“新代码必须合规老代码按模块逐批重构”。先挑一个核心模块做样板跑顺了再铺开。不要一开始就开全量技改那只会让大家抵触。5.5 一个自查清单模板这是我项目里实际在用的精简版自查清单放在PR_TEMPLATE.md里每次提 PR 都要过一遍类型定义是否完整表达了数据边界是否存在any或绕过类型检查的断言组件是否符合“薄、单、纯”所有异步路径是否都有错误处理状态是否存放在最合理的层级测试是否覆盖了核心业务行为而非实现细节是否引入了多余的依赖或过度抽象这七条过完PR 的质量基本就有底了。这套 t3code 规范在团队里跑了三个多月最大的变化不是代码变“标准”了什么而是评审时大家终于开始聊业务逻辑和性能而不是纠结写法。新同学入职看模板比看文档学得快老同学也不用靠记忆和感觉去判断一个 PR 是否合格。我个人在实际使用中最深的体会是编码规范的本质不是限制自由而是把那些反复踩坑的地方用约定焊死让大家把有限的精力花在真正需要创造力的事情上。如果你也在为团队代码风格发散、评审扯皮、重构战战兢兢而头疼与其装一堆插件不如从定一套自己的 t3code 开始。
延伸阅读

更多相关文章

2026/10/27 23:02:08

opencode 深度解析:终端 Agent 生态位、模型接入与 Skill 实战

如果你用过 Claude Code 再回头看 opencode,会有一种“这工具怎么越做越像个活物”的感觉。上篇讲完了基础安装和日常用法,这篇我打算把大家问得最多的几个层面一次性聊透:opencode 在整套 AI 编码工具链里到底处于什么生态位、它的模型服务面…

2026/10/27 22:57:08

BCEWithLogitsLoss多标签分类:形状与dtype避坑

1. 从一次诡异的 loss0.693 说起多标签分类任务里,BCEWithLogitsLoss大概是出镜率最高、也最容易被用错的一个损失函数。我见过太多次类似的场景:模型结构写得没问题,数据管道也跑通了,训练日志里 loss 稳稳地停在 0.693 附近&…

2026/10/27 22:57:08

数据维度解析:从存储组织到特征向量的本质

1. 数据维度的双重面孔:存储组织与特征向量的本质解析在具身智能系统的开发中,我第一次被数据维度的双重含义困扰是在处理机械臂抓取任务的视觉数据时。存储卡里的图像明明是1024x768的JPEG文件,但导入模型后却变成了一个768维的向量——这种…

2026/10/28 0:07:14

Gemini 生成文件入门:在对话里直接产出 Word、Excel 和 PDF

适合人群:经常让 AI 帮忙写方案、整理表格、做学习计划,但每次都要把回答复制到 Word 或表格里再调格式的同学和上班族。本文介绍 Gemini 应用里的“生成文件”功能:在对话中直接说明要什么格式,Gemini 会把结果做成可下载的文件&…

2026/10/28 0:07:14

Dify 入门:用 Docker 自部署并搭建知识库问答助手

适合人群:想在自己电脑或服务器上搭一个“能根据内部文档回答问题”的 AI 助手,会一点命令行、装过 Docker,但不想从零写 RAG 代码的同学和开发者。 Dify 是一个开源的 AI 应用开发平台,可以在网页界面里配置模型、知识库、聊天助…

2026/10/28 0:07:13

Cline 入门:在 VS Code 里用 Plan/Act 模式让 AI 帮你写代码

适合人群:已经装了 VS Code,想试试让 AI 直接在项目里读代码、改文件、跑命令,但又担心 AI 乱改代码的编程初学者。本文介绍开源编程智能体 Cline 的基本用法:安装、接入模型、用 Plan/Act 模式完成小项目、用检查点回退。 Cline …

2026/10/28 0:02:13

基于MATLAB的DTW+VAD语音匹配实战:从序列对齐到孤立词识别

简介:动态时间规整(DTW)算法是一种经典的序列匹配方法,专门用于处理长度不同但形态相似的时序数据;它通过构造二维代价矩阵,利用动态规划搜索最小失真路径,使序列可在时间轴上伸缩对齐&#xff…

2026/10/28 0:02:13

C# 身份证图片信息识别源码:从照片到结构化字段的落地路径

简介:这份资源是面向 C# 与 WinForm 开发者的身份证图片信息识别源码工具,适合需要在本机快速实现证件信息提取的初学者与中级开发者。它调用 Windows 自有接口完成图片识别,再解析出姓名、年龄、出生年月日、身份证号码、民族及住址等关键字…

2026/10/27 14:24:03

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/27 9:34:54

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/27 0:59:21

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/28 0:02:13

Python股票自动交易系统源码解析:从行情获取到风控的完整链路

简介:这份资源是面向高校计算机、金融相关专业毕业设计场景的完整项目源码包,主题为基于Python的股票自动交易系统,适合需要完成毕设选题、课程设计或自学金融量化交易开发的学生与开发者参考。压缩包共收录687个文件,整体约14.93…

2026/10/28 0:02:13

视酷酷信IM源码部署指南:Java 17+MySQL 8.0.33实战配置

简介:这是一套2024年亲测可部署的视酷酷信IM即时通讯系统源码,面向具备Java、OC、C#及Vue开发能力的技术人员,解决私有化即时通讯平台快速搭建与二次定制需求,尤其适用于需集成商城、保障数据主权的中小型企业或独立开发者。资源共…

2026/10/28 0:02:13

Python爬取百度迁徙数据:接口解析、批量采集与避坑指南

简介:这份资源是一个用Python爬取百度迁徙数据的脚本压缩包,面向具备Python基础、希望入门网络爬虫与数据解析的开发者,解决按日期获取全国热门城市人口迁徙数据的问题。包内共1个文件,为单个py脚本,压缩包约1KB&#…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑