1. 从“t3code”这个名字说起:它到底是什么,能解决什么问题
第一次看到“t3code”这个标题,很多人会愣一下——这到底是个工具、一个框架,还是一个代码库的代号?我最初接触它的时候也有同样的疑问。拆开来看,“t3”通常指向“TypeScript 3”或者某种“三层/三端”的架构缩写,而“code”则明确指向代码生成、代码组织或编码规范。综合业内常见的命名习惯,t3code 大概率是一个围绕 TypeScript 生态、面向多端或多层架构的代码生成与工程化方案。它要解决的核心痛点非常明确:在中小型团队里,重复的样板代码、类型定义散落各处、前后端接口对不齐、每次加字段都要手动改五六个文件——这些琐碎但高频的问题,正是 t3code 这类方案瞄准的靶心。
说得再直白一点,t3code 想做的事情就是:你只写一份“源头定义”,剩下的类型、接口、请求封装、校验逻辑、甚至 mock 数据,全部由工具链自动生成。它适合谁?适合那些正在用 TypeScript 做全栈开发、被类型同步折磨过的工程师;适合想给团队建立统一代码规范、又不想引入过重框架的技术负责人;也适合刚接触工程化、想搞明白“代码生成到底怎么落地”的进阶学习者。哪怕你只是一个人写 side project,只要涉及前后端联调,t3code 的思路都能帮你省下大量复制粘贴的时间。
我之所以对这个标题感兴趣,是因为“代码生成”这件事在社区里被讨论了很多年,但真正能落地、不鸡肋的方案并不多。很多生成器要么配置复杂到劝退,要么生成出来的代码没法维护,改一次就再也不敢动。t3code 如果能在“约定优于配置”和“可维护性”之间找到平衡点,那它就值得被认真拆解一遍。接下来的内容,我会从整体设计思路、核心细节、实操流程到踩坑排查,完整地把它讲透,让你看完就能判断它适不适合自己的项目,以及如果适合,该怎么一步步搭起来。
2. 整体设计与思路拆解:为什么是“一份定义,多处生成”
2.1 核心思路:把“重复”交给机器,把“决策”留给人
t3code 这类方案最底层的设计哲学,其实就一句话:凡是能从已有信息推导出来的代码,都不应该手写。你定义了一个用户对象有 id、name、email 三个字段,那么 TypeScript 的 interface、后端的校验规则、前端的表单类型、API 的请求/响应结构,全都是这个定义的“投影”。手写这些投影,除了制造不一致的风险,没有任何价值。
我见过太多项目,后端改了一个字段名,前端没同步,上线后报错才发现。t3code 的思路就是从根上杜绝这种情况:源头只有一份 schema,所有消费方都从这份 schema 生成。这跟 GraphQL 的 codegen、Prisma 的 client 生成、OpenAPI 的 SDK 生成是同一个家族的思想,只是 t3code 更轻、更贴近 TypeScript 原生生态。
为什么选择“生成”而不是“运行时反射”?因为 TypeScript 的类型系统是编译期的,运行时并不存在类型信息。如果靠装饰器或反射在运行时做校验,你会失去编译期的类型安全,还得引入额外的元数据依赖。生成方案的好处是:生成的代码就是普通 TypeScript 代码,IDE 能跳转、能提示、能重构,调试时看到的也是实实在在的文件,而不是黑盒。
2.2 方案选型背后的取舍:轻量 vs 全能
在动手之前,有必要想清楚 t3code 的定位。市面上全能的方案不少,有的把 ORM、路由、校验、文档全包了,但代价是学习曲线陡峭、侵入性强。t3code 如果走轻量路线,那它的优势就是可组合:你可以在现有项目里只引入代码生成这一层,不动其他架构。
我个人的经验是,团队引入新工具最大的阻力从来不是技术难度,而是“改动范围”。如果一个工具要求你把现有代码全部重写,那基本推不动。t3code 这种“增量式”方案就友好很多——你可以先在一个模块试点,生成出来的代码跟手写代码放一起,跑通了再逐步扩大范围。
另一个关键取舍是配置方式。是用 JSON/YAML 配置文件,还是用 TypeScript 文件写配置?我强烈倾向于后者。原因很简单:TypeScript 配置文件有类型提示、能写逻辑、能复用常量,而 JSON 配置写错了只能等运行时报错。t3code 如果支持t3code.config.ts这种形式,那对开发者体验是巨大的加分。
2.3 目录结构与职责划分
一个清晰的目录结构能让生成方案事半功倍。基于常见实践,我建议这样组织:
project/ schema/ # 源头定义,唯一真相 user.ts post.ts generated/ # 生成产物,不手动修改 types/ api/ validators/ t3code.config.ts # 生成器配置 src/ # 业务代码,引用 generated这里有个铁律:generated 目录永远不手动改。一旦你手改了生成产物,下次重新生成就会被覆盖,而且你会陷入“到底以哪份为准”的混乱。正确的做法是改 schema 或改模板,然后重新生成。这条规则听起来简单,但我在实际项目里见过太多人图省事直接改生成文件,最后整个方案崩掉。
提示:可以在 generated 目录放一个 README,写明“此目录由 t3code 自动生成,请勿手动修改”,并在 CI 里加一步校验,确保生成产物与 schema 一致。
3. 核心细节解析与实操要点:schema、模板与生成器三件套
3.1 源头定义怎么写才不容易翻车
schema 是整个方案的地基,地基歪了后面全歪。写 schema 时最容易犯的错是把业务逻辑塞进去。schema 应该只描述“数据长什么样”,不描述“数据怎么处理”。比如用户对象,你只需要定义字段名、类型、是否必填、默认值,至于“注册时发不发邮件”这种逻辑,属于业务层,不该出现在 schema 里。
字段类型的选择也有讲究。能用基础类型就别用复杂类型,能用联合类型就别用 any。我习惯给每个字段加一段注释,说明它的业务含义,因为生成出来的类型定义会带上这些注释,前端同学看代码时能直接理解字段用途,省去大量沟通。
// schema/user.ts export interface UserSchema { /** 用户唯一标识 */ id: string; /** 用户昵称,展示用 */ name: string; /** 邮箱,登录凭证 */ email: string; /** 注册时间戳 */ createdAt: number; /** 账号状态:active 正常,banned 封禁 */ status: 'active' | 'banned'; }注意status用的是字面量联合类型而不是 enum。为什么?因为字面量联合在生成 JSON Schema、生成校验规则时更容易处理,而且编译后不产生额外运行时代码。enum 虽然可读性好,但在跨端场景下容易出问题,尤其是前后端共享时。
3.2 模板引擎的选择与定制
生成器的核心是模板。模板引擎选得好,定制起来轻松;选得不好,改一个格式要折腾半天。常见的选择有 Handlebars、EJS、Mustache 这几种。我的建议是:如果生成逻辑简单,用字符串模板就够了;如果需要条件判断、循环嵌套,Handlebars 是平衡性最好的选择。
模板定制的关键原则是生成可读的代码。有些生成器为了省事,把所有类型塞在一行里,生成出来的文件根本没法看。好的模板应该保留缩进、换行、注释,让生成产物跟手写代码一样整洁。这样即使同事不知道这是生成的,也能正常阅读和维护。
举个例子,生成 API 请求函数的模板大概长这样:
// 生成 {{name}} 相关的 API 请求 export async function get{{pascalName}}(id: string): Promise<{{pascalName}}> { const res = await fetch(`/api/{{kebabName}}/${id}`); if (!res.ok) { throw new Error(`获取 {{name}} 失败: ${res.status}`); } return res.json(); }这里pascalName、kebabName是命名转换的辅助函数,把userProfile转成UserProfile和user-profile。命名转换是代码生成里最容易被忽视但最影响体验的细节,一定要在配置里统一处理。
3.3 生成器的执行流程与增量策略
生成器什么时候跑?三种时机:手动跑、监听文件变化自动跑、CI 里跑。手动跑适合调试模板阶段,改一次跑一次看效果。监听模式适合日常开发,schema 一改,生成产物立刻更新,IDE 里马上能看到新类型。CI 里跑是为了兜底,防止有人忘了重新生成就提交。
增量策略也很重要。如果每次生成都全量覆盖,那大项目里生成一次可能要好几秒。好的生成器会对比文件内容,只有变化了才写入,这样既快又不会触发不必要的文件监听事件。我实测下来,增量生成能把一次生成从 3 秒降到 200 毫秒以内,体验差别巨大。
注意:增量生成要处理好“删除”场景。如果 schema 里删了一个字段,生成产物里对应的类型也要删掉,不能残留。这需要生成器记录上次生成的文件清单,对比后清理孤儿文件。
4. 实操过程与核心环节实现:从零搭一套可用的生成链路
4.1 环境准备与依赖安装
先把基础环境搭起来。假设你已经有一个 TypeScript 项目,Node 版本建议 18 以上,因为要用到一些较新的 API。安装依赖这一步没什么花哨的,但版本选择有讲究。
npm install -D t3code typescript tsxtsx是用来直接运行 TypeScript 配置文件的,省去编译步骤。如果你用的是 pnpm 或 yarn,命令对应调整即可。安装完后,在package.json里加两个脚本:
{ "scripts": { "gen": "t3code generate", "gen:watch": "t3code generate --watch" } }gen用于一次性生成,gen:watch用于开发时监听。这里我特意把命令名起得短,因为这两个命令你一天要敲几十次,短一点能省不少事。
4.2 配置文件编写与参数详解
配置文件是整个方案的控制中心。我建议用t3code.config.ts,因为可以享受类型提示。一个典型的配置大概是这样:
import { defineConfig } from 't3code'; export default defineConfig({ // schema 文件所在目录 schemaDir: './schema', // 生成产物输出目录 outputDir: './generated', // 生成目标:类型、API、校验器 targets: ['types', 'api', 'validators'], // 命名转换规则 naming: { typeName: 'pascal', fileName: 'kebab', }, // 是否在生成文件头部加注释 banner: true, // 增量生成 incremental: true, });每个参数背后都有考量。targets决定生成哪些产物,如果你暂时不需要校验器,去掉它能加快生成速度。naming统一命名风格,避免同一个概念在不同文件里叫法不一致。banner加上“此文件自动生成”的注释,提醒同事别手改。incremental开启增量,大项目必备。
4.3 完整生成流程演示
假设我们有一个schema/user.ts,定义了 UserSchema。运行npm run gen后,生成器会做这几件事:
第一步,扫描schemaDir下所有文件,解析出每个 schema 的字段信息。这一步会做类型推断,比如string对应 TypeScript 的string,number对应number,字面量联合对应字面量联合。
第二步,对每个 schema,按targets生成对应产物。类型产物输出到generated/types/user.ts,API 产物输出到generated/api/user.ts,校验器输出到generated/validators/user.ts。
第三步,对比已有文件,只写入有变化的部分。如果generated/types/user.ts内容没变,就跳过写入,避免触发文件监听。
第四步,清理孤儿文件。如果 schema 里删了某个定义,对应的生成文件也会被删除。
生成出来的类型文件大概是这样:
// generated/types/user.ts // 此文件由 t3code 自动生成,请勿手动修改 /** 用户唯一标识 */ export type UserId = string; /** 用户完整信息 */ export interface User { id: UserId; name: string; email: string; createdAt: number; status: 'active' | 'banned'; }注意这里额外生成了一个UserId类型别名。这是个小技巧:把 id 抽成独立类型,后续如果 id 的格式变了(比如从 string 变成带前缀的模板字面量类型),只需要改一处。这种“类型别名提取”是生成器可以自动做的优化,能显著提升代码的可维护性。
4.4 在业务代码中消费生成产物
生成产物怎么用?直接 import 就行。比如前端组件里:
import type { User } from '@/generated/types/user'; import { getUser } from '@/generated/api/user'; function UserCard({ id }: { id: string }) { const [user, setUser] = useState<User | null>(null); useEffect(() => { getUser(id).then(setUser); }, [id]); if (!user) return <div>加载中...</div>; return <div>{user.name}</div>; }这里User类型和getUser函数都来自生成产物,你不需要手写任何接口定义。后端改了字段,重新生成后前端立刻能感知到类型变化,编译期就能发现不兼容的地方。这就是“一份定义,多处生成”的威力。
提示:在 tsconfig 里给
generated目录配一个路径别名,比如@/generated/*,这样 import 路径短,重构时也方便。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 生成产物与手写代码冲突怎么办
这是最常见的问题。你生成了一个User类型,但项目里已经有一个手写的User类型,两者冲突了。解决办法有三个:一是改生成产物的命名前缀,比如生成ApiUser;二是把旧的手写类型迁移到 schema 里,统一由生成器管理;三是用命名空间隔离。
我推荐第二种,因为长期来看,同一个概念有两份定义迟早会出问题。迁移的时候可以渐进式来,先把新代码用生成类型,旧代码慢慢改,不要一次性全换。
5.2 生成速度慢的排查思路
如果生成一次要好几秒,先看是不是全量生成。开启incremental后应该快很多。如果还是慢,检查 schema 文件是不是太多太碎。我建议一个业务域一个 schema 文件,不要一个字段一个文件。另外,模板里如果有复杂的字符串拼接或正则,也会拖慢速度,可以先把模板简化,确认是模板问题还是 IO 问题。
5.3 类型推断错误的处理
有时候生成出来的类型跟预期不符,比如可选字段变成了必填。这通常是 schema 写法的问题。检查一下字段有没有加?,或者有没有在配置里设置默认的可选策略。我习惯在 schema 里显式标注可选性,不依赖默认值,这样最不容易出错。
下面这张表整理了我踩过的典型问题和对应解法:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 生成产物没更新 | 增量缓存未失效 | 删除缓存目录或加--force参数 |
| 类型冲突 | 手写类型与生成类型重名 | 迁移手写类型到 schema 或加前缀 |
| 生成速度慢 | 全量生成或模板复杂 | 开启增量,简化模板逻辑 |
| 可选字段变必填 | schema 未标注可选 | 显式加?或调整默认策略 |
| 孤儿文件残留 | 增量清理未生效 | 检查生成器版本,手动清理一次 |
5.4 团队协作中的规范约定
工具再好,团队不遵守规范也白搭。我建议定几条硬规矩:第一,schema 变更必须走 code review,因为它是源头;第二,generated 目录加入.gitignore还是提交?我倾向于提交,这样 CI 和本地环境一致,但要在 CI 里加一步校验,确保生成产物与 schema 同步;第三,禁止手改 generated 目录,发现一次就在群里发红包(这招比什么规范都管用)。
注意:如果 generated 目录提交到仓库,每次 schema 变更都会产生大量 diff,review 时容易看花眼。可以在 review 时先看 schema 的 diff,再看生成产物的 diff,分开看会清晰很多。
6. 扩展玩法与个人实践体会
t3code 这套思路跑通之后,能扩展的方向其实很多。比如把 schema 同时生成 OpenAPI 文档,前端后端共用一份定义,文档永远不会过期。再比如生成 mock 数据工厂,测试时直接调用,不用手写一堆假数据。还可以生成数据库迁移脚本,schema 一变,迁移文件自动出来。
我自己在实际项目里用类似方案最大的体会是:前期多花一天搭生成链路,后期每天省两小时。尤其是接口字段频繁变动的阶段,手写类型的人还在改第五个文件,用生成方案的人已经跑完测试了。当然,它也不是银弹,schema 设计得不好,生成出来的东西一样难用。所以花时间打磨 schema 和模板,比急着生成更重要。
最后分享一个小技巧:给生成器加一个--dry-run模式,只打印将要生成的内容而不写文件。调试模板的时候特别有用,你能快速看到改动效果,又不会污染工作目录。这个功能很多生成器默认没有,但自己加一个也就几十行代码,性价比极高。