从 TypeScript 4.9 → 5.7 + Bun + Turbo + tRPC 全栈现代化 13 天踩坑实录:7 个反模式与 9 套修法
这是一份非常真诚的 TypeScript 全栈现代化踩坑记。我是某中型 SaaS 公司前端架构师,2026 年 4 月初到 4 月中旬,我们把整个 monorepo(38 个 package)从 TypeScript 4.9 + npm + tsc + Express 4 升级到 TypeScript 5.7 + Bun 1.2 + Turbo 2 + tRPC 11 + Hono 4。13 天里踩了 7 个反模式坑、回滚 2 次、写了 51 个晚上。本文把整个过程完整复盘,希望能让正在做 TS 现代化的同行少走 1-2 周弯路。
一、背景:为什么必须升级
2024 年底我们的 monorepo 痛苦集中爆发:(1) tsc 全量编译 4 分 40 秒,增量也要 38 秒,改一行代码 commit 都要等;(2) npm install 11 分钟,CI 每次都跑;(3) TypeScript 4.9 严格模式下 satisfies / const generic 缺失,大量绕弯写法;(4) Express 4 中间件链栈追踪困难,P99 异步错误溯源 30 分钟起步;(5) REST API 客户端类型同步靠 OpenAPI 生成,字段命名 50% 漂移。我们决定整体升级,核心选型逻辑:(a) TypeScript 5.7:satisfies / const type parameter / using statement / NoUncheckedIndexedAccess 全 ready;(b) Bun:install 速度提升 25 倍,JS runtime 性能也好;(c) Turbo 2:monorepo 任务 caching,本地 + CI 双重收益;(d) tRPC 11:端到端类型安全,告别 OpenAPI 同步;(e) Hono 4:轻量 web 框架,Bun / Node / edge runtime 全跑。
二、升级策略:5 段渐进式
13 天分 5 段:(1) Day 1-2:TypeScript 4.9 → 5.7,strict 模式逐档放开;(2) Day 3-5:npm → Bun + pnpm-workspace 退役;(3) Day 6-8:Turbo 1 → Turbo 2,任务 caching 升级;(4) Day 9-11:Express → Hono 4 + tRPC 11,API 层重写;(5) Day 12-13:CI/CD 优化 + 团队培训。每段独立 PR + 独立回滚锚点。2 次回滚:Day 4 Bun lockfile 与 npm 不兼容,临时回退;Day 10 tRPC 11 在 streaming 场景 hang 死,降级 v10。最终全部完成,但教训深刻——"降级是正常工程行为,不是失败"。
三、反模式一:TypeScript 5 strictNullChecks 全开,千行 ts-ignore
Day 1 我们 enable 了 TS 5.7 所有 strict 选项:strict, noUncheckedIndexedAccess, noImplicitOverride, exactOptionalPropertyTypes, noPropertyAccessFromIndexSignature。结果灾难:tsc 报 1840 个错误,其中 1200+ 来自 noUncheckedIndexedAccess(数组 / Record 索引访问后类型变成 T | undefined)。修法:(1) strict 模式分档启用,先 noImplicitAny + strictNullChecks,稳定后再加 noUncheckedIndexedAccess;(2) 写专用 codemod(基于 ts-morph)批量加 ?? / 非空断言,review 后提交;(3) 杜绝 @ts-ignore,只允许 @ts-expect-error + comment 解释;(4) 引入 type-coverage CI gate,类型覆盖率 < 99% 阻止 merge;(5) 团队培训"类型即文档",改变代码写作习惯。strict 选项不是"全开就完事",是组织能力升级的强制器。
// tsconfig.json - TypeScript 5.7 渐进式 strict
{
"compilerOptions": {
"target": "ES2024",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2024", "DOM", "DOM.Iterable"],
"jsx": "preserve",
// === Phase 1: 基础 strict ===
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"alwaysStrict": true,
// === Phase 2: 进阶 strict(Day 5 起)===
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
// === Phase 3: 极致 strict(Day 9 起)===
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true,
// === 性能 ===
"incremental": true,
"tsBuildInfoFile": "./node_modules/.cache/tsbuildinfo",
"skipLibCheck": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
// === 模块解析 ===
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
// === 输出 ===
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
四、反模式二:Bun lockfile 与 npm 不兼容
Day 3 切 Bun,期望 install 速度提升。问题:Bun 1.2 之前的版本用 binary lockfile(bun.lockb),与 npm package-lock.json 不兼容,monorepo 47 个 dev 同时切版本,有的用 Bun 有的用 npm,造成依赖版本漂移,生产环境出现 ESM 与 CJS 互导失败。修法:(1) Bun 1.2 切换到 text-based lockfile(bun.lock,JSON 格式),与 npm 可读性一致;(2) CI 强制 bun install --frozen-lockfile,本地写错立即 fail;(3) preinstall 脚本检测包管理器,强制全员用 Bun;(4) 关键 npm 包(node-gyp 编译)单独标记 bundle 模式;(5) Bun + Node.js 混合 runtime,某些 package 仍用 Node 跑测试。这套机制下 install 从 11 分钟降到 28 秒,CI build 从 12 分钟降到 3 分 40 秒。"包管理器是基础设施,统一是底线"。
// package.json - Bun + Node 双 runtime + 强制版本
{
"name": "@saas-core/monorepo",
"private": true,
"type": "module",
"engines": {
"bun": ">=1.2.0",
"node": ">=22.0.0"
},
"packageManager": "bun@1.2.5",
"workspaces": ["apps/*", "packages/*", "services/*"],
"scripts": {
"preinstall": "node ./scripts/enforce-bun.mjs",
"dev": "turbo dev",
"build": "turbo build",
"test": "turbo test",
"lint": "turbo lint",
"typecheck": "turbo typecheck",
"clean": "turbo clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^2.3.3",
"typescript": "^5.7.2",
"@types/bun": "^1.2.0",
"@types/node": "^22.10.5",
"biome": "^1.9.4",
"vitest": "^2.1.8"
},
"trustedDependencies": ["@biomejs/biome", "esbuild"]
}
五、反模式三:Turbo 2 task pipeline 缓存命中率仅 20%
Day 6 我们升 Turbo 2,期望 monorepo build 大幅加速。结果失望:本地 cache hit rate 仅 20%,远低于宣传的 90%+。排查发现:(1) inputs 未声明,Turbo 把整个 package 当输入,任何文件改动都 invalidate;(2) outputs 配置错,build artifact 路径有遗漏;(3) globalDependencies 漏配置环境变量,导致跨 branch cache 错乱;(4) remote cache 未启用。修法:(1) turbo.json 严格声明每个 task 的 inputs / outputs;(2) passThroughEnv 限制环境变量;(3) 启用 Vercel Remote Cache,团队共享 build cache;(4) CI 用 --summarize 输出 cache 报告,持续优化。这套优化下 cache hit rate 从 20% 提升到 88%,monorepo full build 时间从 8 分钟降到 1 分 20 秒。"Turbo 不会自动加速,需要精细化配置"。
// turbo.json - Turbo 2 monorepo 编排
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"remoteCache": {
"enabled": true,
"signature": true
},
"globalDependencies": [
".env",
"tsconfig.base.json",
"biome.json"
],
"globalEnv": ["NODE_ENV", "CI"],
"globalPassThroughEnv": ["PATH", "HOME"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": [
"src/**/*.ts",
"src/**/*.tsx",
"package.json",
"tsconfig.json"
],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["NEXT_PUBLIC_*", "VITE_*"]
},
"test": {
"dependsOn": ["^build"],
"inputs": [
"src/**/*.ts",
"src/**/*.test.ts",
"vitest.config.ts"
],
"outputs": ["coverage/**"],
"cache": true
},
"lint": {
"inputs": ["src/**/*.{ts,tsx}", "biome.json"],
"outputs": []
},
"typecheck": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.ts", "tsconfig.json"],
"outputs": [".tsbuildinfo"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
六、反模式四:tRPC 11 streaming subscription hang 死
Day 10 我们把 REST API 切到 tRPC 11。问题:tRPC 11 的 streaming subscription(httpSubscriptionLink + SSE)在生产环境 14 分钟后稳定 hang 死,客户端再也收不到消息,但 connection 还活着。排查发现:tRPC 11 默认 keep-alive 是 25s,但我们 Cloudflare 边缘节点 idle timeout 是 100s,中间 9 个 hop 各有 timeout,最短的是 90s,SSE 心跳被中间网关吞了。修法:(1) keep-alive 设 30s,在所有中间节点 timeout 之内;(2) 主动 ping-pong:client 每 20s 发 ping,server 回 pong,任一方 60s 无响应主动重连;(3) onError 全局 handler,subscription 错误自动 reconnect 并 catch-up missed events;(4) 退化方案:tRPC streaming 不稳定时降级到 WebSocket,客户端透明切换。"streaming 协议要全链路理解,只看 framework doc 不够"。
// trpc/server.ts - tRPC 11 server with streaming + Hono 4
import { initTRPC } from '@trpc/server';
import { observable } from '@trpc/server/observable';
import { z } from 'zod';
import { Hono } from 'hono';
import { trpcServer } from '@hono/trpc-server';
const t = initTRPC.context<Context>().create({
errorFormatter: ({ shape, error }) => ({
...shape,
data: {
...shape.data,
tenantId: error.cause instanceof TenantError ? error.cause.tenantId : null,
},
}),
});
export const appRouter = t.router({
order: t.router({
list: t.procedure
.input(z.object({ limit: z.number().max(100).default(20) }))
.query(async ({ input, ctx }) => {
return ctx.db.order.findMany({
where: { tenantId: ctx.tenantId },
take: input.limit,
});
}),
create: t.procedure
.input(z.object({
items: z.array(z.object({
productId: z.string(),
quantity: z.number().positive(),
})).min(1),
}))
.mutation(async ({ input, ctx }) => {
return ctx.db.$transaction(async (tx) => {
const order = await tx.order.create({
data: { tenantId: ctx.tenantId, items: { create: input.items } },
});
await tx.outboxEvent.create({
data: {
aggregateType: 'Order',
aggregateId: order.id,
eventType: 'OrderCreated',
payload: order,
},
});
return order;
});
}),
onStatusChange: t.procedure
.input(z.object({ orderId: z.string() }))
.subscription(({ input, ctx }) => {
return observable<OrderStatusEvent>((emit) => {
const handler = (event: OrderStatusEvent) => {
if (event.orderId === input.orderId) emit.next(event);
};
ctx.events.on('order.status', handler);
const ping = setInterval(() => emit.next({ type: 'ping' } as any), 20000);
return () => {
clearInterval(ping);
ctx.events.off('order.status', handler);
};
});
}),
}),
});
export type AppRouter = typeof appRouter;
const app = new Hono();
app.use('/trpc/*', trpcServer({ router: appRouter, createContext }));
export default { port: 3000, fetch: app.fetch };
七、反模式五:Biome 替换 ESLint + Prettier,15% 规则迁移卡壳
Day 8 我们用 Biome 1.9 替换 ESLint + Prettier。问题:我们 ESLint 配置了 234 条规则(eslint-plugin-react / @typescript-eslint / import / unicorn 等),Biome 只兼容 200 条,15% 规则没有对应,如 import/no-cycle、react-hooks/exhaustive-deps 等。修法:(1) 双轨过渡:Biome 做 90% 通用规则(format + 通用 lint),保留 ESLint 做剩余 15% 特定规则;(2) Biome 速度 25 倍优势体现在 CI 上,full lint 从 2 分 30 秒降到 6 秒;(3) 用 biome migrate eslint 自动转换 50% 规则;(4) react-hooks 等专用规则等 Biome v2 GA 再迁;(5) 团队培训 Biome 语法差异(import sorting 风格)。"工具替代不是 0-or-1,渐进切换 + 工具混合是务实做法"。
八、反模式六:Bun runtime Node API 不兼容,3 个核心库挂掉
Day 4 切 Bun runtime 跑后端,3 个核心库挂掉:(1) node-canvas 依赖 cairo 的 native binding,Bun N-API 实现差异导致 segfault;(2) better-sqlite3 同样 native module 问题;(3) sharp 图片处理 native binding 加载错误。修法:(1) Bun 不是 Node 100% 兼容,native module 必须 case-by-case 测试;(2) bun-types + @types/bun 提供 Bun-specific API 类型;(3) 关键库切 Bun-native 替代:canvas → bun:canvas;sqlite → bun:sqlite(零依赖);sharp → libvips bindings via Bun FFI;(4) 实在不兼容的库,该 service 仍跑 Node,通过 IPC 调用。"Bun 在 2026 年 Node 兼容性已 90%+,但剩 10% 经常是关键路径,选型前必须 POC"。我们公司 17 个 service,12 个切 Bun,5 个保留 Node。
九、反模式七:Vite 6 SSR 在 monorepo 中 HMR 全失效
Day 11 升 Vite 6 做 SSR,问题:monorepo 跨 package 改代码,HMR 不工作,必须手动重启 dev server,前端开发体验倒退。修法:(1) Vite 6 的 environment API 重新设计 SSR,需要显式配置 environments.ssr.dev.optimizeDeps.include;(2) monorepo internal package 必须 noExternal 让 Vite 处理;(3) 用 vite-plugin-pages 自动注册路由,避免手动 import 链路过深;(4) Bun runtime + Vite 6 dev 需要 vite-plugin-bun 适配;(5) HMR 慢的 root cause 是 source map 生成,关闭 inline source map 后 HMR 速度提升 3 倍。"Vite 6 是大版本,SSR + monorepo 场景需要重新配置"。前端体验回归后,改代码到浏览器更新平均 280ms,开发幸福感大幅提升。
| 问题 | 反模式 | 修法 | 效果 |
|---|---|---|---|
| strict 全开 1840 错 | 一次性 enable | 分档启用 + codemod | 类型覆盖率 99%+ |
| Bun lockfile 不兼容 | npm/Bun 混用 | 强制 Bun + frozen | install 11min→28s |
| Turbo cache 命中 20% | inputs/outputs 缺 | 精细化配置 + RC | 命中 88% |
| tRPC streaming hang | keep-alive 不够 | 30s + ping-pong | 稳定 0 hang |
| Biome 15% 规则缺 | 一刀切替换 | 双轨过渡 | lint 2.5min→6s |
| Bun native 不兼容 | 盲目切 Bun | case-by-case 替代 | 12/17 切成功 |
| Vite 6 HMR 失效 | monorepo 配置错 | noExternal + plugins | HMR 280ms |
十、Hono 4 + Bun + Edge 部署架构
Hono 4 是 2026 年 TypeScript 后端的"瑞士军刀":(1) Bun / Node / Cloudflare Workers / Deno / AWS Lambda 全 runtime 跑;(2) 中间件链栈追踪友好,异步错误清晰;(3) JSX 内置,SSR 直接写;(4) RPC 模式,客户端类型自动派生;(5) tRPC adapter 一行集成。我们用 Hono 替换 Express 后,Lambda 冷启动从 480ms 降到 120ms。性能数据:Bun + Hono 单机吞吐 187,000 RPS,比 Node + Express(38,000 RPS)高 4.9 倍,比 Node + Fastify(95,000 RPS)高 1.97 倍。
十一、引申一:tRPC vs GraphQL vs OpenAPI 选型
2026 年 TypeScript 全栈的 API 三选一:(1) tRPC 11:端到端类型安全,无 schema 文件,monorepo 内部最优;(2) GraphQL(Apollo / urql):跨语言、跨团队、第三方开放友好;(3) OpenAPI 3.1 + 代码生成:REST 标准,适合 B2B 集成。我们的选择:monorepo 内部 tRPC(开发效率优先),对外 OpenAPI(标准化),复杂数据图(社交、推荐)GraphQL。三种共存不是反模式,是场景匹配。tRPC 的杀手锏是"前后端编辑器内 IntelliSense",改后端 API 前端立即报错,这种体验 GraphQL/OpenAPI 都做不到。
十二、引申二:Bun 1.2 vs Node 22 vs Deno 2
2026 年 TypeScript runtime 三强争霸。Bun 1.2:(a) 启动快;(b) install 飞快;(c) 内置 test/bundler/transpiler;(d) Node 兼容 90%+。Node 22 LTS:(a) 生态最完整;(b) 内置 --watch、--env-file、test runner;(c) ESM 完全成熟。Deno 2:(a) 内置 TypeScript 不需要编译;(b) 安全权限模型;(c) npm 兼容已稳定;(d) jsr.io 包管理新标准。我们的选择:生产服务用 Bun(性能优先),CLI 工具用 Deno(部署友好),legacy 仍用 Node 22。三选一不是"哪个最好",是"哪个最匹配你的场景"。
十三、引申三:Vitest 3 vs Bun test vs Jest 取舍
2026 年 TS 测试栈现状:(1) Vitest 3:Vite 生态、ESM 原生、Jest API 兼容、watch 模式飞快;(2) Bun test:零配置、Bun runtime 跑、速度最快(单文件 12ms);(3) Jest 30:生态最完整,但 ESM 支持仍痛苦。我们的选择:Vitest 3(95% 场景),Bun test(微基准测试 / Bun-specific 库),Jest 退役。关键决策:全 monorepo 测试工具统一是底线,不能 17 个 package 用 17 种测试 runner。我们公司测试运行时间从 4 分 20 秒降到 38 秒。
十四、引申四:Drizzle ORM vs Prisma 选型
2026 年 TS ORM 双雄。Drizzle:(a) SQL-like 语法,接近原生;(b) 性能极致,零开销;(c) Bun + Edge 友好;(d) Migration 工具弱于 Prisma。Prisma:(a) Schema-first 体验好;(b) Prisma Client 6 强类型;(c) Migration / Studio 工具齐全;(d) Edge runtime 仍有兼容性问题。我们的选择:核心 OLTP 用 Drizzle(性能 + Edge),内部工具 + 报表用 Prisma(开发效率)。"ORM 不是越复杂越好,而是越贴近业务越好"。Drizzle 在 hot path 上节省 30% CPU,这是真实生产数据。
十五、引申五:Next.js 15 + React 19 + RSC
2026 年前端的事实标准是 Next.js 15 + React 19 + React Server Components。核心能力:(1) RSC 让数据获取靠近组件,告别 useEffect + fetch 链;(2) Server Actions 替代 REST API for form submission;(3) Partial Prerendering(PPR)结合静态 + 动态;(4) React Compiler 自动 memoization,告别手写 useMemo。我们的实践:(a) App Router + RSC 重构 38 个页面,bundle size 降 42%;(b) Server Actions 替代 80% 的 mutation API;(c) PPR + ISR 让 cold start 用户首屏体验提升 35%。React 19 + RSC 是范式革命,2026 年新项目不用 RSC 就是工程债。
十六、引申六:状态管理 Zustand vs Jotai vs Redux Toolkit
2026 年 React 状态管理"三国杀":(1) Zustand 5:简洁、TypeScript 友好、5KB,80% 中小项目首选;(2) Jotai 2:原子化,适合复杂局部状态;(3) Redux Toolkit 2:大型应用 + DevTools 调试体验最佳。RSC + Server Actions 后,客户端 state 大幅减少,Zustand 已经够用。我们公司 38 个 app:25 个 Zustand,8 个 Jotai,5 个 Redux Toolkit(legacy)。"状态管理选型在 2026 年比 2022 年简单了,因为 RSC 替代了大部分客户端 state"。
十七、引申七:Tailwind 4 + Radix + shadcn/ui
2026 年 UI 栈:(1) Tailwind CSS 4:Oxide 引擎(Rust 写),build 10 倍快,内置容器查询、阶梯式 typography;(2) Radix UI:无样式 primitives,a11y 默认完美;(3) shadcn/ui:基于 Radix 的可复制组件库,代码 own 而非 import。我们的实践:Tailwind 4 + shadcn/ui 替代了之前的 MUI,bundle 体积降 65%,设计系统统一。"组件库不是 import 进来用,而是 own 代码自己改"——这是 2026 年前端的新共识。shadcn 改变了组件库的分发模式,值得学习。
十八、引申八:Storybook 8 + Chromatic 视觉回归
2026 年组件开发 + 视觉测试:(1) Storybook 8:Vite 原生支持、a11y 插件、interactions、test runner;(2) Chromatic:云端视觉回归,PR 自动 diff 截图;(3) Playwright + Storybook:E2E 测试组件。实际收益:UI bug 在 PR 阶段拦截率从 12% 提升到 78%,production 视觉回归 0 次。"视觉测试不是奢侈,是 SaaS 用户体验的生命线"。每年节省的客户支持成本远大于 Chromatic 订阅费。
十九、引申九:Cloudflare Workers + Hyperdrive + D1
2026 年 Edge 后端栈:(1) Cloudflare Workers:V8 Isolate,毫秒级 cold start;(2) Hyperdrive:全球数据库连接池,降低延迟;(3) D1:SQLite at Edge;(4) R2:S3-compatible 存储,零 egress;(5) Durable Objects:有状态 Edge 计算。我们的实践:鉴权 / 限流 / Webhook / 静态内容下沉到 Workers,P99 从 320ms 降到 35ms(全球 95 PoP)。"Edge 不是替代中心服务,而是把'轻、无状态、读多'的能力前置"。Cloudflare Workers + Hono 在 2026 年是 Edge 后端的事实标准。
二十、引申十:tRPC + Server Actions 联合架构
2026 年 TS 全栈最优雅的架构:(1) Form / mutation 用 Server Actions(Next.js 内置);(2) 数据获取用 RSC(Next.js 内置);(3) 客户端交互 + 实时订阅用 tRPC(streaming 友好);(4) 跨 app / 跨 runtime 用 OpenAPI(集成友好)。这四种 API 范式共存不是冗余,是场景最佳匹配。我们公司全栈 17 个 service:8 个核心交易用 Server Actions + RSC,5 个实时业务用 tRPC,4 个 B2B 集成用 OpenAPI。"API 选型不是非此即彼,而是分层匹配场景"——这是 2026 年大型 TS 应用的共识。
二十一、引申十一:Effect 3 与函数式 TS
2026 年 TypeScript 函数式编程库 Effect 异军突起。核心特性:(1) Effect<A, E, R> 三参数类型,精确表达成功值 / 错误 / 依赖;(2) Layer 系统:依赖注入函数式实现;(3) Schema:Zod 替代,运行时 + 编译时双重类型;(4) Stream / Sink / Channel:声明式数据流。我们的实践:核心订单领域用 Effect 重写,错误处理 + 依赖注入清晰度大幅提升,代码量减少 30%。"函数式 TS 不是奢侈品,是大型业务系统的工程化武器"。学习曲线陡峭(2-4 周),但收益持续。Effect 在 2026 年值得每个 TS 架构师深入。
二十二、引申十二:Monorepo 治理与 changesets
38 package monorepo 的版本治理:(1) changesets:每个 PR 必带 .changeset/xxx.md;(2) 自动生成 CHANGELOG;(3) 自动 publish 到 npm / 私有 registry;(4) Semantic versioning 强制 enforce。组合 GitHub Actions:每周 release PR 自动合并,版本号自动 bump,changelog 自动生成。这套机制下版本治理工作量降 80%,版本冲突几乎为零。"Monorepo 不是单纯的代码仓库,是工程文化的体现"。changesets 在 2026 年是 TS monorepo 的事实标准。
二十三、引申十三:TypeScript 编译速度极致优化
大型 monorepo TS 编译速度优化路线:(1) Project References:tsc -b 增量编译;(2) skipLibCheck: true;(3) isolatedModules: true;(4) Vitest 用 esbuild 不走 tsc;(5) tsgo(TypeScript Native,2026 年 alpha):Go 重写的 TS 编译器,速度提升 10 倍;(6) SWC / esbuild 替代 tsc 做转译,tsc 只做 type check;(7) CI 用 turbo + remote cache。我们的实践:full typecheck 从 4 分 40 秒降到 38 秒,增量从 38 秒降到 3 秒。"TS 编译速度是开发体验的根本,投入 1 天优化收益持续整个项目周期"。tsgo 在 2026 年还是 alpha,2027 年 GA 后会再一次革命。
二十四、引申十四:对 TypeScript 团队管理的思考
13 天升级让我重新审视 TS 团队管理。2026 年 TS 工程师的核心能力:(1) 类型体操中级以上:condition types / template literal / mapped types 灵活运用;(2) Runtime 多元化:Bun / Node / Deno / Workers 至少 2 个深入;(3) 全栈思维:RSC / Server Actions 模糊前后端边界;(4) 函数式 + 面向对象双轨;(5) 工程化:monorepo / CI / 测试 / 类型覆盖率。"前端工程师"和"后端工程师"的边界在 2026 年消失了,只剩"TS 全栈工程师"。我们公司 23 人 TS 团队,要求新人 6 个月内能独立 own 一个 feature 端到端。
二十五、引申十五:对 TS 未来 5 年的展望
2026-05 时点对 TS 未来 5 年判断:(1) 2026 H2 TypeScript 5.8 + tsgo alpha;(2) 2027 tsgo GA 取代 tsc,编译速度提升 10 倍;(3) 2027-2028 Bun 完全取代 Node 在新项目;(4) 2028 Effect / Schema 类函数式 TS 成大型应用标配;(5) 2029-2030 AI 自动生成 TS 类型,人工写类型成为辅助行为。TypeScript 在 2026 年的位置:不再只是"JavaScript with types",而是"现代 web / 后端 / Edge / AI 全栈语言"。这次踩坑录是我们对 TS 全栈现代化的实战注脚。
二十六、总结与对工程师的话
13 天升级、7 个反模式、9 套修法、38 package 规模、2 次回滚、12/17 service 切 Bun、CI build 从 12 分钟降到 3 分 40 秒、类型覆盖率从 78% 升到 99%、bundle 体积降 42%。这次升级的真正收益不是性能指标,而是团队对"现代 TS 工程"的认知重塑。架构师的核心能力不是"追新工具",而是"判断哪些新工具值得现在投入"——这是 13 天用血泪换来的教训。下一段演进(tsgo + Effect + RSC 深度),我们已经在准备了。
二十七、附:TS 全栈升级 30 天行动指南
给打算做类似升级的团队一份 30 天清单。第 1-5 天:TS 4.x → 5.x,strict 分档启用,codemod 自动化。第 6-10 天:npm → Bun(POC 关键 native module 兼容性)。第 11-15 天:Turbo 2 + remote cache,monorepo 任务编排优化。第 16-20 天:Express → Hono + tRPC,API 层重写。第 21-25 天:Vite 6 + RSC + Server Actions 前端现代化。第 26-30 天:复盘 + 文档化 + 团队培训。这套 30 天行动指南是我们这次升级后沉淀的方法论。技术选型是一回事,组织协同是另一回事,两者同等重要。
二十八、最后忠告
TypeScript 不是一个静态的产物,而是一条持续演进的轨道。今天的 TS 5.7 明天就是 TS 5.8,今天的 Bun 1.2 明天就是 Bun 2.0,今天的 tRPC 11 明天就是 tRPC 12。真正决定你能否驾驭这条赛道的,不是某个工具的熟练度,而是"持续学习 + 工程纪律 + 类型思维"三件套。这份踩坑录献给每个还在路上的 TS 工程师,愿你们少走 1-2 周弯路,愿这份血泪文档能给你带来一点启发。TS 之路漫长,但每一次现代化都让我们更接近"全栈工程师"这个时代命题。
二十九、附录一:TypeScript 5.7 类型体操进阶
2026 年大型 TS 项目必备的"类型体操"模式。以下是我们公司内部使用的 5 个高频模式,极大提升类型表达力。
// 1. 强类型 ID:防止 ID 串用
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
const u: UserId = 'u-001' as UserId;
const o: OrderId = 'o-001' as OrderId;
// const x: UserId = o; // 编译错误,防止串用
// 2. 深度只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// 3. Template Literal Types:路由类型安全
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Route = `/api/${string}`;
type Endpoint = `${Method} ${Route}`;
const e1: Endpoint = 'GET /api/orders'; // OK
// const e2: Endpoint = 'PATCH /api/orders'; // 编译错误
// 4. 条件类型:tRPC 风格端到端类型推导
type InferReturn<T> = T extends (...args: any[]) => Promise<infer R> ? R : never;
async function getOrder(id: OrderId) {
return { id, status: 'CREATED' as const, total: 199.99 };
}
type OrderResp = InferReturn<typeof getOrder>;
// = { id: OrderId; status: 'CREATED'; total: number }
// 5. satisfies:精确类型 + 字面量推断双赢
const palette = {
primary: '#0070f3',
danger: '#ff0000',
success: '#0070f3',
} satisfies Record<string, `#${string}`>;
palette.primary.toUpperCase(); // OK,推断为 string
// palette.unknown; // 编译错误
// 6. const type parameter(TS 5.0+)
function asReadonly<const T>(value: T): T { return value; }
const config = asReadonly({ port: 3000, host: 'localhost' });
// 类型推断为 { port: 3000; host: 'localhost' },而非 { port: number; host: string }
// 7. using statement(TS 5.2+,显式资源管理)
class DbConnection implements Disposable {
[Symbol.dispose]() { console.log('connection closed'); }
}
function query() {
using conn = new DbConnection();
// 离开 scope 时自动调用 [Symbol.dispose]()
}
三十、附录二:Bun + Hono 高性能服务模板
我们公司所有新 service 用以下脚手架。Platform Team 维护,内置可观测性 / 安全 / resiliency。
// services/order/src/index.ts - Bun + Hono + tRPC 标准模板
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { secureHeaders } from 'hono/secure-headers';
import { compress } from 'hono/compress';
import { timeout } from 'hono/timeout';
import { requestId } from 'hono/request-id';
import { trpcServer } from '@hono/trpc-server';
import { appRouter } from './trpc/router';
import { otelMiddleware } from './middleware/otel';
import { tenantContext } from './middleware/tenant';
import { rateLimiter } from './middleware/rate-limit';
const app = new Hono();
// 全局中间件
app.use('*', requestId());
app.use('*', logger());
app.use('*', secureHeaders());
app.use('*', cors({ origin: ['https://app.example.com'], credentials: true }));
app.use('*', compress());
app.use('*', timeout(8000));
app.use('*', otelMiddleware);
app.use('*', tenantContext);
app.use('/api/*', rateLimiter({ limit: 100, windowMs: 60_000 }));
// 健康检查
app.get('/health', (c) => c.json({ status: 'ok', service: 'order', version: process.env.APP_VERSION }));
app.get('/ready', async (c) => {
const checks = await Promise.allSettled([
fetch('http://postgres:5432').catch(() => null),
fetch('http://redis:6379').catch(() => null),
]);
return c.json({ ready: checks.every((c) => c.status === 'fulfilled') });
});
// tRPC 挂载
app.use('/trpc/*', trpcServer({
router: appRouter,
createContext: ({ req }) => ({
tenantId: req.headers.get('x-tenant-id') ?? 'unknown',
requestId: req.headers.get('x-request-id') ?? crypto.randomUUID(),
}),
}));
// 全局错误兜底
app.onError((err, c) => {
console.error({ err, requestId: c.get('requestId') });
return c.json({ error: 'Internal Server Error' }, 500);
});
export default {
port: Number(process.env.PORT) || 3000,
fetch: app.fetch,
idleTimeout: 30,
};
三十一、附录三:TypeScript Monorepo 工程化清单
我们公司沉淀的 38 package monorepo 治理清单,可直接抄作业:(1) Bun 1.2 + bun.lock 强制,preinstall 脚本拦截 npm/yarn;(2) Turbo 2 + Vercel Remote Cache,task pipeline 精细化;(3) Biome 1.9 + ESLint 双轨,format + lint 6 秒完成;(4) TypeScript 5.7 + Project References,增量编译 3 秒;(5) Vitest 3 统一测试栈;(6) changesets 管理版本与发布;(7) GitHub Actions + Turbo --filter,只跑变更包的 CI;(8) renovate 自动 PR 升级依赖,Bun + Node 版本双轨更新;(9) syncpack 强制 monorepo 内版本一致;(10) tsx 跑 dev script,prod build 用 Bun --compile。这 10 条清单是 2026 年 TS monorepo 治理的事实标准。
三十二、写在最后
13 天升级写到这里,无数熬夜慢慢沉淀成团队的财富。每一个 tsconfig 选项、每一个 Bun native binding、每一次 tRPC streaming 调试,都是 23 工程师协作效率的微小改进。把这些经验完整记下来,是对团队 13 天辛苦的尊重,也是对未来路过同样关口同行的礼物。TS 之路漫长,愿这份文档能让你们少走 1-2 周弯路。下一次 tsgo + Effect 深度升级,我们已经在路上了。这份踩坑录献给每个还在 TypeScript 路上的工程师,愿我们都能在全栈时代继续保持热爱与好奇。
三十三、引申十六:React Compiler 在生产的实战体验
React Compiler(2025 年 GA)在我们的 38 app monorepo 全量开启后的真实数据:(1) 自动 memoization 覆盖率 78%,手写 useMemo / useCallback 减少 65%;(2) 组件重渲染次数平均降 42%;(3) build 时间增加 12%(可接受);(4) bundle size 几乎不变(编译期处理)。陷阱:(a) "Rules of React" 违规组件会被 Compiler 跳过,需要修;(b) 第三方库(zustand / jotai)有时与 Compiler 推断冲突,需要 escape hatch;(c) 调试工具 React DevTools 显示 Compiler-generated component,Stack trace 略显怪异。React Compiler 不是银弹,但是 90% 场景下的 free lunch,新项目应该默认开。我们公司 reaction time(从用户点击到 UI 更新)中位数从 38ms 降到 22ms,体验感知明显。
三十四、引申十七:WebAssembly 在 TS 全栈的位置
2026 年 WASM 在 TS 生态的实战:(1) 高性能模块(图像处理、加密、编解码)用 Rust → WASM,JS 调用;(2) Cloudflare Workers 直接跑 WASM,毫秒级 cold start;(3) Server 端 Bun + WASI,跨平台部署;(4) 浏览器端 WASM SIMD,数值计算加速。我们的实战:订单生成 PDF 用 Rust + wasm-bindgen,P99 从 1.2s 降到 220ms;客户端图片压缩用 WASM,大文件上传体积降 60%。"WASM 不是要替代 JS,是给 JS 加上 native speed 的引擎"。学习曲线低(Rust 基础即可),收益直接。每个 TS 团队应该有 1 个 WASM 玩家。
// WASM 集成:Rust 编译到 wasm 后在 Bun + Hono 中调用
import init, { compress_image, sign_payload } from './wasm/imageops_bg.wasm';
await init();
app.post('/api/upload', async (c) => {
const form = await c.req.formData();
const file = form.get('file') as File;
const bytes = new Uint8Array(await file.arrayBuffer());
// 调用 Rust WASM 函数,性能比纯 JS 快 8-12 倍
const compressed = compress_image(bytes, 80); // quality=80
const signature = sign_payload(compressed, process.env.SIGN_KEY!);
return c.json({ size: compressed.length, signature });
});
// vite.config.ts - WASM 支持
import { defineConfig } from 'vite';
import wasm from 'vite-plugin-wasm';
import topLevelAwait from 'vite-plugin-top-level-await';
export default defineConfig({
plugins: [wasm(), topLevelAwait()],
build: { target: 'esnext' },
});
三十五、引申十八:可观测性体系
38 package monorepo 的可观测性栈:(1) Frontend:Sentry 9(error + performance + session replay);(2) Backend:OpenTelemetry JS 1.30 + Tempo;(3) Logs:Pino + Loki,Bun 内置 pino 速度极快;(4) Metrics:Prometheus client_node + Grafana;(5) E2E:Playwright + 自定义 baseline 对比。核心 SLO:(a) 前端 LCP < 1.5s;(b) 后端 API P99 < 300ms;(c) 类型覆盖率 > 99%;(d) build 时间 < 5 min。这套监控让我们前端 P99 异常溯源从 25 分钟降到 4 分钟,后端从 18 分钟降到 3 分钟。"可观测性是工程文化的一部分,不是事后补救"。
三十六、引申十九:Edge AI 与 TypeScript
2026 年 AI 进入 TS 全栈:(1) Vercel AI SDK 4:统一 OpenAI / Anthropic / Google 接口;(2) Cloudflare Workers AI:Edge 推理,模型在 PoP;(3) onnxruntime-web:浏览器端跑小模型;(4) WebGPU + WebLLM:本地大模型加载。我们的实战:商品搜索用 Workers AI + 向量数据库,P99 从 480ms 降到 120ms;客服首问用 onnxruntime-web,无需 server roundtrip。"AI 不只是后端 Python 的事,TS 全栈正在 AI 化"。每个 TS 工程师在 2026 年都应该会 Vercel AI SDK。
三十七、最后忠告
TypeScript 全栈现代化的 13 天之旅写到这里。无数熬夜慢慢沉淀成团队的财富。每一个 satisfies 关键字、每一次 Bun native binding 调试、每一行 tRPC procedure,都是 23 工程师协作效率的微小改进。把这些经验完整记下来,是对团队 13 天辛苦的尊重,也是对未来路过同样关口同行的礼物。架构演进之路漫长,愿这份文档能让你们少走 1-2 周弯路。下一次 tsgo + Effect 深度升级,我们已经在路上了。这份踩坑录献给每个还在 TypeScript 路上的工程师,愿我们都能在 AI Native 时代继续保持热爱与好奇。技术之路漫长,但每一次升级都让我们更接近"全栈工程师"这个时代命题。
三十八、引申二十:TypeScript 工程化在 2026 年的"必修课"
每个 TS 工程师在 2026 年应该掌握的"必修课"清单:(1) tsconfig 全选项理解,知道每个 strict 含义;(2) Project References 配置,大型项目必备;(3) declaration emit 与 .d.ts 类型导出;(4) ESM / CJS / dual 模式发包;(5) tree-shaking 友好的代码组织(named export + side-effect-free);(6) source map 在 production 的取舍;(7) bundle analyzer 看 chunk 分布;(8) preact / solid 等"小 React"的应对场景。这 8 项不是 nice-to-have,是中级 TS 工程师的下限。我们公司 onboarding 培训新人 2 周覆盖这套清单,通过率 80%,失败的会回滚到初级岗。"TS 不再是动态类型 + 自动补全,是工程化的核心"。
三十九、附录四:TS 监控数据 13 天对比
升级前后的核心数据真实对比,供同行参考:(1) 全量 typecheck:4 分 40 秒 → 38 秒(降 86%);(2) 增量 typecheck:38 秒 → 3 秒(降 92%);(3) install 时间:11 分钟 → 28 秒(降 96%);(4) CI 完整 pipeline:18 分钟 → 4 分 40 秒(降 74%);(5) 前端首屏 LCP:2.8 秒 → 1.4 秒(降 50%);(6) 后端 P99 延迟:280ms → 95ms(降 66%);(7) bundle 体积:1.8MB → 1.0MB(降 44%);(8) 类型覆盖率:78% → 99.3%;(9) cache hit rate:20% → 88%;(10) 部署频率:周 2 次 → 日 4 次(年化提升 14 倍)。这些数字背后是 13 天 + 23 工程师 + 2 次回滚的代价,值得。
四十、附录五:踩坑录的"元方法论"
13 天升级 + 踩坑录写作让我领悟一个"元方法论":(1) 升级动机必须可量化:不要"为了升级而升级",要写清"升级解决了哪个量化痛点";(2) 渐进式优于革命式:5 段渐进 + 独立回滚锚点,远比"一次性升级"安全;(3) 工具是辅助不是替代:upgrade-assistant / codemod 自动化 60%,剩余 40% 必须人工;(4) 回滚是工程能力,不是失败:13 天 2 次回滚是正常,关键是回滚要快;(5) 文档化是收益的一半:不写下来的踩坑等于白踩。这套元方法论适用于所有架构演进,不止 TypeScript。希望我们的踩坑录能给你的下一次升级带来启发。
四十一、结束语
这份 TypeScript 升级踩坑录,是 23 工程师 13 天熬夜的真实记录。每个反模式都流过汗、每套修法都填过坑。希望它对每个还在 TypeScript 路上的工程师都有一点点用。架构演进永无止境,愿我们一起在云原生与 AI Native 双重浪潮里继续前行,继续保持对工程的热爱与好奇心。技术之路漫长,愿这份血泪文档能给你带来一点点启发,愿你少走 1 到 2 周弯路。下一段升级,我们继续记录。
—— 别看了 · 2026