我的博客
返回首页

TypeBox + Elysia Eden 类型膨胀

分类:4-全栈/01-elysia
发布于:
阅读时间:35 分钟

🔍 一、问题根源分析

你遇到的"每级 API 都要推断 3 秒"的核心原因是:

1. TypeBox Schema 类型爆炸

TypeBox 的

Static<typeof schema>
会生成极其复杂的嵌套类型。当你的后端有多个路由、每个路由有复杂的请求/响应 Schema 时,Eden 需要:

  • 遍历所有路由
  • 解析每个路由的 Method、Path、Body、Headers、Query、Response
  • 生成一个巨大的联合类型树
// 这就是 Eden 内部在做的事情(简化版)
type EdenType<App> = {
  [Path in keyof App['router']]: {
    [Method in keyof App['router'][Path]]: {
      body: Static<App['router'][Path][Method]['body']>
      response: Static<App['router'][Path][Method]['response']>
      // ... 还有 headers, query, params 等
    }
  }
}

当路由数量多、Schema 复杂时,这个类型会呈指数级膨胀

2. 项目引用配置问题

如果你前后端共用一个

tsconfig.json
,或者引用配置不当,会导致:

  • 前端修改代码时,后端类型重新计算
  • Eden 类型每次都要重新从
    typeof app
    推断

🛠️ 二、诊断工具(先确认瓶颈)

1. 使用
tsc --diagnostics
查看耗时

# 在后端目录运行
cd src/server
tsc --noEmit --diagnostics

# 在前端目录运行
cd src/client  
tsc --noEmit --diagnostics

输出会显示类似:

Files:                         150
Lines:                       12000
Identifiers:                 25000
Symbols:                     18000
Types:                       15000  ← 关注这个
Instantiation count:         80000  ← 这个过高说明类型计算复杂

2. 使用
--explainFiles
查看类型来源

tsc --noEmit --explainFiles > explain.txt

然后搜索

elysia
typebox
,看有多少文件被包含。

3. VSCode 类型性能监控

在 VSCode 中打开命令面板,运行:

TypeScript: Open TS Server Log

查看是否有慢查询日志。


✅ 三、优化方案(按优先级排序)

方案 1:前后端分离 + 预生成类型(最推荐)⭐⭐⭐⭐⭐

核心思路:后端独立编译,生成

.d.ts
类型文件,前端只引用类型,不重新推断。

目录结构

project/
├── src/
│   ├── server/          # 后端独立项目
│   │   ├── tsconfig.json
│   │   ├── index.ts     # 导出 App 类型
│   │   └── app.ts       # Elysia app 定义
│   └── client/          # 前端独立项目
│       ├── tsconfig.json
│       └── api.ts       # 只引用类型,不推断
├── tsconfig.json        # 根配置(使用 references)
└── package.json

后端
src/server/tsconfig.json

{
  "compilerOptions": {
    "composite": true,           // 关键:启用复合项目
    "declaration": true,         // 关键:生成 .d.ts
    "declarationMap": true,      // 生成类型映射
    "outDir": "../../dist/server",
    "rootDir": ".",
    "skipLibCheck": true,
    "incremental": true,         // 增量编译
    "tsBuildInfoFile": "../../.tsbuildinfo/server"
  },
  "include": ["./**/*"]
}

后端
src/server/index.ts

// 关键:显式导出类型,不要让前端重新推断
import { Elysia } from 'elysia'
import { t } from '@elysiajs/eden'

export const app = new Elysia()
  .get('/api/user', () => ({ id: 1, name: 'test' }))
  .post('/api/post', ({ body }) => body, {
    body: t.Object({ title: t.String(), content: t.String() })
  })

// 关键:导出预计算的类型,而不是 typeof app
export type App = typeof app
export type UserResponse = { id: number; name: string }
export type PostBody = { title: string; content: string }

tsconfig.json

{
  "files": [],
  "references": [
    { "path": "./src/server" },
    { "path": "./src/client" }
  ]
}

前端
src/client/tsconfig.json

{
  "compilerOptions": {
    "composite": true,
    "outDir": "../../dist/client",
    "rootDir": ".",
    "skipLibCheck": true,
    "incremental": true,
    "tsBuildInfoFile": "../../.tsbuildinfo/client"
  },
  "include": ["./**/*"],
  "references": [
    { "path": "../server" }  // 引用后端项目
  ]
}

前端
src/client/api.ts

// 关键:直接导入类型,不要用 treaty 实时推断
import { treaty } from '@elysiajs/eden'
import type { App } from '../../server/index'

// 这样类型已经预计算好了,不会重新推断
export const api = treaty<App>('http://localhost:3000')

// 使用时类型秒出
const { data } = await api.api.user.get()

方案 2:简化 TypeBox Schema(减少类型复杂度)⭐⭐⭐⭐

TypeBox 的嵌套 Schema 是类型膨胀的元凶。优化方法:

❌ 避免深层嵌套

// 糟糕:类型会非常复杂
const schema = t.Object({
  user: t.Object({
    profile: t.Object({
      settings: t.Object({
        preferences: t.Object({ ... })
      })
    })
  })
})

✅ 扁平化 + 类型别名

// 推荐:使用类型别名拆分
const UserSchema = t.Object({ id: t.Number(), name: t.String() })
const ProfileSchema = t.Object({ bio: t.String(), avatar: t.String() })

const schema = t.Object({
  user: t.Ref(UserSchema),
  profile: t.Ref(ProfileSchema)
})

// 导出独立类型
export type User = Static<typeof UserSchema>
export type Profile = Static<typeof ProfileSchema>

✅ 使用
t.Unsafe
简化复杂类型

// 对于非常复杂的响应类型,手动定义类型
export type ComplexResponse = {
  id: string
  data: Record<string, unknown>
  meta: { total: number; page: number }
}

// 运行时验证用 TypeBox,类型用手动定义
const schema = t.Object({
  id: t.String(),
  data: t.Record(t.String(), t.Unknown()),
  meta: t.Object({ total: t.Number(), page: t.Number() })
})

// 这样 Eden 推断时会简单很多

方案 3:Eden 类型缓存(避免重复推断)⭐⭐⭐

创建一个类型缓存层:

// src/server/types.ts
import type { App } from './index'
import type { treaty } from '@elysiajs/eden'

// 预计算一次,导出缓存类型
export type CachedEdenApp = ReturnType<typeof treaty<App>>

// 或者更激进:直接定义接口
export interface ApiClient {
  api: {
    user: {
      get: () => Promise<{ data: { id: number; name: string } }>
    }
    post: {
      post: (body: { title: string; content: string }) => Promise<{ data: any }>
    }
  }
}

前端直接使用缓存类型:

import type { ApiClient } from '../../server/types'
// 不再依赖 treaty 推断

方案 4:TypeScript 配置优化(必做)⭐⭐⭐⭐⭐

后端
tsconfig.json
关键配置

{
  "compilerOptions": {
    "skipLibCheck": true,        // 跳过 node_modules 类型检查(提速 50%+)
    "incremental": true,         // 增量编译
    "composite": true,           // 复合项目
    "declaration": true,         // 生成声明文件
    "declarationMap": true,      // 类型映射(调试用)
    "noEmit": false,             // 必须输出
    "isolatedModules": true      // 模块隔离(加速增量)
  }
}

前端
tsconfig.json
关键配置

{
  "compilerOptions": {
    "skipLibCheck": true,
    "incremental": true,
    "composite": true,
    "noEmit": false
  },
  "references": [
    { "path": "../server" }
  ]
}

📊 四、构建命令优化

使用

tsc -b
(build mode) 进行增量构建:

# 根目录执行
tsc -b  # 自动按依赖顺序构建

# 监听模式(只编译变化的部分)
tsc -b -w

# 强制重新构建(清理缓存后)
tsc -b --force

package.json
中:

{
  "scripts": {
    "build:server": "tsc -b src/server",
    "build:client": "tsc -b src/client",
    "build": "tsc -b",
    "dev:server": "tsc -b src/server -w",
    "dev:client": "tsc -b src/client -w"
  }
}

🎯 五、最终建议

优化项预期收益实施难度
skipLibCheck: true
类型检查速度 +50%
composite + references
增量编译,只改哪里编哪里⭐⭐
预生成类型导出Eden 推断时间 3s → 0.1s⭐⭐
TypeBox Schema 扁平化类型复杂度降低 70%⭐⭐⭐
使用
tsc -b
构建
构建时间减少 80%

立即执行的步骤:

  1. 今天:在前后端
    tsconfig.json
    中添加
    skipLibCheck: true
    incremental: true
  2. 明天:拆分前后端为独立复合项目,配置
    references
  3. 后天:在后端导出预计算的类型,前端直接引用
  4. 本周:优化 TypeBox Schema,减少嵌套深度

如果完成以上优化后仍然慢,可以考虑:

  • 使用 类型性能分析工具(如
    typeprof
  • 考虑 不使用 Eden,直接用
    fetch
    + 手动类型定义(牺牲便利性换性能)
  • 将大型 Schema 拆分成多个小文件,减少单次推断的类型数量

需要我帮你具体分析你的

tsconfig.json
配置或 TypeBox Schema 结构吗?

类型诊断分析