📚 TypeScript 教程系列

  1. 入门与配置
  2. 基础类型与变量声明
  3. 函数
  4. 流程控制与运算符
  5. 集合类型
  6. 异步编程与错误处理
  7. 接口与类
  8. 泛型与类型组合
  9. 高级类型
  10. 模块、装饰器与工程化
  11. TypeScript 与 Bun 实战(本文)
  12. TypeScript 与 React 实战

前面的章节我们系统学习了 TypeScript 的类型系统与工程化能力,但一直停留在"编译成 JS 后在浏览器或沙箱里跑"的层面。本篇把 TypeScript 带入 Bun 服务端场景:从项目初始化、tsconfig 针对 Bun 的关键配置,到内置模块类型、ESM/CJS 互操作、HTTP 服务、环境变量校验,最后落地一个可运行、可调试、可部署的 REST API 项目,帮助你把类型系统的价值真正兑现到后端工程中。

为什么在 Bun 中使用 TypeScript

Bun 原生就能执行 TypeScript——不需要 tsc 预编译,也不需要 tsx 这类转译层。(Node 23.6+ 也通过 type stripping 支持直接 node file.ts 运行 .ts,但仅限可擦除语法;Bun 的转译器更完整,能处理 enumnamespace 等需要代码生成的特性。)那为什么还要在 Bun 端认真对待 TypeScript?核心原因有三点:

  1. 类型安全前移:接口入参、数据库模型、第三方 API 返回值在编译期就能被检查,避免运行时才发现 undefined 取属性、字段拼错这类低级却高频的 bug。
  2. 重构有底气:改一个函数签名,编译器会列出所有受影响的调用点;在大型后端项目里,这比"全局搜索 + 祈祷"可靠得多。
  3. IDE 体验:跳转定义、自动补全、内联文档都依赖类型信息,Bun 内置模块和 @types/bun 让服务端开发也能享受前端一样的智能提示。

💡 成本提示:Bun 已经把 TS 转译成本降到几乎为零——运行时零配置、零额外依赖。你只需要在 CI 或 git hook 里跑一次 tsc --noEmit 做类型检查即可。相比 Node + tsx 或 Node 23.6+ 的 strip-types 方案,Bun 的转译器对 TS 语法的容忍度更高(见下文),并省去了额外运行时依赖。

环境搭建

初始化项目

bun init 一键生成 TypeScript 项目脚手架:

1
2
mkdir ts-bun-app && cd ts-bun-app
bun init

bun init 会自动生成 package.jsontsconfig.json(已针对 Bun 优化)、.gitignore 等文件。接着安装 Bun 的类型定义:

1
2
# @types/bun 是 Bun 内置 API(Bun.serve、Bun.file、bun:sqlite 等)的类型来源
bun add -d @types/bun

@types/bun 让 TS 认识 Bun 全局对象、bun:sqlite 等 Bun 专属模块。没有它,编辑器会对 Bun.serveBun.file 等 API 报类型错误。

Bun 生成的 tsconfig.json

bun init 生成的 tsconfig.json 已经是针对 Bun 优化的推荐配置。下面是一个精简版,逐项解释会在下一节展开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"types": ["bun"],

// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,

// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true
},
"include": ["src"]
}

运行 TS 的方式

Bun 原生执行 TypeScript,不需要任何中间转译工具。常见方式如下:

方式命令适用场景是否产出 JS
直接运行bun src/index.ts开发与生产均可❌ 内存转译
监听运行bun --watch src/index.ts开发期热重启❌ 内存转译
热重载bun --hot src/index.ts开发期保留状态重载❌ 内存转译
打包bun build发布 CLI / 库✅ 单文件
1
2
3
4
5
6
7
8
9
10
11
# 方式一:直接运行(开发与生产都可用)
bun src/index.ts

# 方式二:监听文件变化自动重启
bun --watch src/index.ts

# 方式三:热重载(保留模块状态,重启更快)
bun --hot src/index.ts

# 方式四:打包发布
bun build src/index.ts --outdir dist --target bun

⚠️ Bun 转译 ≠ 类型检查:Bun 运行时只做转译(剥离类型注解),不做类型检查。类型错误不会阻止程序运行。因此推荐在 CI 和 git hook 中单独执行 bunx tsc --noEmit 做类型检查,把"编译通过即可信"的保障留在构建流水线里。

💡 可擦除语法与 --erasableSyntaxOnly:TS 5.6 引入 --erasableSyntaxOnly 选项,强制源码只含“可擦除语法”(类型注解等可直接删除、无需代码生成的语法),专门适配 Node 23.6+ 这类纯类型剥离运行时——它们遇到 enum、构造器参数属性等需要转换的语法会报错。Bun 的转译器做的是完整转译而非单纯剥离,能直接处理上述不可擦除语法,因此不受此限制。

tsconfig 针对 Bun 的关键选项

Bun 的运行时行为与 Node 不同,决定了 tsconfig 不能照搬传统 Node 配置。下面逐项说明。

target 与 lib

Bun 基于 JavaScriptCore 引擎,支持最新的 ES 特性,因此 targetlib 都可以设为 ESNext,无需降级:

1
2
3
4
5
6
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext"] // 不包含 "DOM",避免误用浏览器 API
}
}

module 与 moduleResolution

Bun 内部使用打包器风格的模块解析,因此推荐 module: "Preserve" + moduleResolution: "bundler"。其中 module: "Preserve" 是 TS 5.4 引入的新选项,专为打包器/原生 TS 运行时设计——保留导入写法、不做模块格式转换,与 moduleResolution: "bundler" 配合使用。这个组合允许你:

  • 导入 .ts 文件时带扩展名allowImportingTsExtensions: true
  • 使用顶层 await、JSX 等 TS 默认不允许的特性
  • 不强制 ESM 中写 .js 扩展名(Bun 运行时会自动解析)
1
2
3
4
5
6
7
8
{
"compilerOptions": {
"module": "Preserve",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true
}
}

💡 与 Node 的区别:Node 运行时要求 ESM 导入带 .js 扩展名,因此 Node 端推荐 moduleResolution: "NodeNext"。Bun 不需要——它的模块解析器与打包器一致,导入时写 .ts 扩展名也可以,甚至可以省略扩展名。

strict 与严格族

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"strict": true, // 总开关,开启下面全部
"noUncheckedIndexedAccess": true, // 索引访问返回 T | undefined
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true
}
}

strictstrictNullChecksnoImplicitAnystrictFunctionTypes 等的合集。noUncheckedIndexedAccess 不在 strict 内,但对后端非常推荐——它让 arr[i]obj[key] 的返回类型带上 undefined,强制你处理"键不存在"的情况,恰好对应数据库查询、JSON 解析等高频场景。

noEmit 与类型检查分离

1
2
3
4
5
{
"compilerOptions": {
"noEmit": true // Bun 负责转译和运行,tsc 只做类型检查
}
}

Bun 的推荐配置中 noEmit: true 是核心设计:Bun 运行时负责转译执行,tsc 只负责类型检查。两者职责分离,互不干扰。你不需要 outDirrootDir 等输出配置——除非你要用 tsc 编译产出 JS 供其他运行时使用。

Node 内置模块的类型使用

Bun 兼容绝大多数 Node.js 内置模块(fspathprocesshttp 等),安装 @types/bun 后,这些模块和 Bun 专属 API 都能获得完整类型提示。下面通过几个高频模块演示。

fs —— 文件系统

优先使用 fs/promises 的异步 API,避免阻塞事件循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { readFile, writeFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';

// 读取文件,泛型已由 @types/bun 推断为 string
async function readConfig(path: string): Promise<string> {
const content = await readFile(path, 'utf-8');
return content;
}

// 写入文件
await writeFile('./data.json', JSON.stringify({ ok: true }), 'utf-8');

// 同步检查存在性(仅用于启动期一次性判断)
if (!existsSync('./data.json')) {
console.warn('配置文件缺失,将使用默认值');
}

💡 Bun 专属 API:Bun 还提供了 Bun.file() 返回一个 BunFile 对象,支持流式读取、获取文件信息等,性能优于传统 fs API:

1
2
3
4
const file = Bun.file('./data.json');
const text = await file.text(); // 直接读取为字符串
const json = await file.json(); // 直接解析为 JSON
const size = file.size; // 文件大小(不读入内存)

若 JSON 是构建期已知的静态资源,还可用 TS 5.3 的 import attributes 做类型安全导入(需在 tsconfig 开启 resolveJsonModule):

1
import data from './data.json' with { type: 'json' };

path —— 路径处理

跨平台拼接路径务必用 path,不要手写字符串拼接(Windows 是 \,POSIX 是 /):

1
2
3
4
5
6
7
8
9
import path from 'node:path';

const dataFile = path.join(process.cwd(), 'data', 'config.json');
// Windows: C:\project\data\config.json
// POSIX: /project/data/config.json

// 解析扩展名与目录
const ext = path.extname(dataFile); // .json
const dir = path.dirname(dataFile); // .../data

process —— 进程与环境

1
2
3
4
5
6
7
8
9
// 命令行参数:process.argv[0] 是 bun 路径,[1] 是脚本路径,[2] 起才是用户参数
const [,, filePath] = process.argv;
if (!filePath) {
console.error('用法: bun index.ts <file>');
process.exit(1); // 非零退出码表示异常
}

// 当前工作目录与平台
console.log(process.cwd(), process.platform);

ESM 下的 __dirname / __filename

CJS 中可直接用 __dirname__filename,但在 ESM(package.json"type": "module")下它们不存在。Bun 提供了更简洁的替代方案:

1
2
3
4
5
6
7
8
9
10
// Bun 专属:import.meta.dir 和 import.meta.path(无需手动派生)
const __dirname = import.meta.dir;
const __filename = import.meta.path;

// 兼容写法:从 import.meta.url 派生(与 Node 通用)
import { fileURLToPath } from 'node:url';
import path from 'node:path';

const __filename_compat = fileURLToPath(import.meta.url);
const __dirname_compat = path.dirname(__filename_compat);

💡 @types/bun 已内置全局类型:安装 @types/bun 后,Bun 全局对象、import.meta.dirimport.meta.path 等 Bun 专属 API 都有完整类型提示,无需额外声明。

ESM 与 CommonJS 互操作

Bun 同时支持 ESM 和 CJS,且互操作比 Node 更顺滑——Bun 的转译器会自动抹平大部分差异。在 TS 中,模块系统的选择由 package.json"type" 字段决定:

1
2
3
4
// package.json
{
"type": "module" // "commonjs" 或缺省则为 CJS
}

配合 tsconfigmoduleResolution: "bundler",TS 不会强制要求导入路径带特定扩展名。两种模式下导入写法的差异:

1
2
3
4
5
6
7
8
9
10
// CommonJS(package.json 无 type 或 "type": "commonjs")
const fs = require('node:fs'); // OK
import fs from 'node:fs'; // OK

// ESM("type": "module")
import fs from 'node:fs'; // OK
import { readFileSync } from 'node:fs'; // OK
// Bun 中导入相对路径可以省略扩展名,也可以带 .ts
import { helper } from './utils'; // OK(Bun 自动解析)
import { helper } from './utils.ts'; // OK(allowImportingTsExtensions)

⚠️ 与 Node 的差异:Node 的 ESM 运行时强制要求相对导入写 .js 扩展名,即便源文件是 .ts。Bun 不需要——它的模块解析器更灵活,这是从 Node 迁到 Bun 时体验提升最明显的地方之一。

互操作场景(CJS 项目导入 ESM 包,或反之)在 Bun 中几乎无感——Bun 的转译器会自动处理 requireimport 的混用,无需动态 import() 的变通写法:

1
2
3
// Bun 中可以直接在 CJS 项目里 import ESM 包,无需 await import()
import chalk from 'chalk';
console.log(chalk.green('成功'));

环境变量与配置校验

后端项目严重依赖环境变量(端口、数据库连接、密钥)。process.env 的所有值都是 string | undefined,直接使用既不安全也不友好。推荐用 Zod 在启动期校验并转换为强类型配置对象。

Bun 默认会自动加载项目根目录的 .env 文件到 process.env,无需额外安装 dotenv

1
2
bun add zod
# @types/bun 已作为开发依赖安装,无需额外安装 dotenv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/config.ts
import { z } from 'zod';

// Bun 自动加载 .env,无需 import 'dotenv/config'

// 定义环境变量的 schema
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().int().positive().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(16),
});

// 启动期校验,失败立即抛出明确错误
const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
console.error('❌ 环境变量校验失败:');
console.error(parsed.error.flatten().fieldErrors);
process.exit(1);
}

// 导出强类型配置,全项目复用
export const env = parsed.data;
// ^? { NODE_ENV: ...; PORT: number; DATABASE_URL: string; JWT_SECRET: string }

使用处即可享受类型推断:

1
2
3
4
5
6
7
8
import { env } from './config';

const server = Bun.serve({
port: env.PORT,
fetch() {
return new Response(`服务运行于 ${env.NODE_ENV} 模式,端口 ${env.PORT}`);
},
});

实战:用 Bun.serve 构建 REST API

下面把前面所有知识点串联成一个完整的待办事项(Todo)API。Bun 内置了高性能 HTTP 服务器 Bun.serve,无需安装 Express 等第三方框架。

类型定义

先定义数据模型与请求/响应类型,遵循"类型先行"原则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/types.ts
export type TodoStatus = 'todo' | 'in_progress' | 'done';

export interface Todo {
id: number;
title: string;
status: TodoStatus;
createdAt: string;
}

// 创建请求体:不需要 id 与时间戳
export interface CreateTodoInput {
title: string;
status?: TodoStatus; // 可选,默认 'todo'
}

// 更新请求体:所有字段可选
export interface UpdateTodoInput {
title?: string;
status?: TodoStatus;
}

// 统一 API 响应包装
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}

仓储层

用一个内存数组模拟数据库,把数据访问封装起来,便于将来替换为真实数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/repository.ts
import type { Todo, CreateTodoInput, UpdateTodoInput } from './types';

const store: Todo[] = [];
let nextId = 1;

export const todoRepo = {
findAll(): Todo[] {
return [...store]; // 返回副本,避免外部直接修改
},

findById(id: number): Todo | undefined {
return store.find((t) => t.id === id);
},

create(input: CreateTodoInput): Todo {
const todo: Todo = {
id: nextId++,
title: input.title,
status: input.status ?? 'todo',
createdAt: new Date().toISOString(),
};
store.push(todo);
return todo;
},

update(id: number, input: UpdateTodoInput): Todo | undefined {
const todo = this.findById(id);
if (!todo) return undefined;
Object.assign(todo, input); // 仅覆盖传入字段
return todo;
},

remove(id: number): boolean {
const idx = store.findIndex((t) => t.id === id);
if (idx === -1) return false;
store.splice(idx, 1);
return true;
},
};

路由与服务

Bun.serve 内置了基于路径的路由器(Bun 1.2+),支持静态路由、参数路由和通配符,且路由参数有类型安全的自动补全:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// src/app.ts
import { todoRepo } from './repository';
import { env } from './config';
import type { ApiResponse, CreateTodoInput, UpdateTodoInput, Todo } from './types';
import type { BunRequest } from 'bun';

// 辅助函数:统一响应格式
function json<T>(status: number, payload: ApiResponse<T>): Response {
return Response.json(payload, { status });
}

export const server = Bun.serve({
port: env.PORT,
routes: {
'/todos': {
// 列出全部
GET: () => json<Todo[]>(200, { success: true, data: todoRepo.findAll() }),

// 创建
POST: async (req) => {
const body = (await req.json()) as CreateTodoInput;
const { title, status } = body ?? {};
if (!title || typeof title !== 'string') {
return json(400, { success: false, error: 'title 必填且为字符串' });
}
const todo = todoRepo.create({ title, status });
return json<Todo>(201, { success: true, data: todo });
},
},

'/todos/:id': {
// 获取单个(路由参数有类型提示)
GET: (req: BunRequest<'/todos/:id'>) => {
const id = Number(req.params.id);
if (Number.isNaN(id)) {
return json(400, { success: false, error: 'id 必须是数字' });
}
const todo = todoRepo.findById(id);
if (!todo) {
return json(404, { success: false, error: '待办不存在' });
}
return json(200, { success: true, data: todo });
},

// 更新
PUT: async (req: BunRequest<'/todos/:id'>) => {
const id = Number(req.params.id);
const body = (await req.json()) as UpdateTodoInput;
const todo = todoRepo.update(id, body ?? {});
if (!todo) {
return json(404, { success: false, error: '待办不存在' });
}
return json(200, { success: true, data: todo });
},

// 删除
DELETE: (req: BunRequest<'/todos/:id'>) => {
const id = Number(req.params.id);
if (todoRepo.remove(id)) {
return json(200, { success: true });
}
return json(404, { success: false, error: '待办不存在' });
},
},
},

// 未匹配路由的兜底处理
fetch() {
return json(404, { success: false, error: '路由不存在' });
},

// 错误处理
error(err) {
console.error(err);
return json(500, { success: false, error: '服务器内部错误' });
},
});

启动入口

1
2
3
4
5
// src/index.ts
import { env } from './config';
import { server } from './app';

console.log(`🚀 Todo API 已启动: http://localhost:${server.port}`);

测试运行:

1
2
3
4
bun src/index.ts
# 另开终端
curl -X POST http://localhost:3000/todos -H 'Content-Type: application/json' -d '{"title":"学习 TS"}'
curl http://localhost:3000/todos

💡 Bun.serve vs ExpressBun.serve 基于 uWebSocket,性能远超 Express。路由参数有 TypeScript 类型推断(req.params.id 自动补全),无需手动声明 Request<{ params: { id: string } }>。如果你已有 Express 项目,Bun 也能直接运行——bun add express 后按传统写法即可,Bun 兼容 Express 及绝大多数 Node 生态包。

package.json scripts 配置

把常用命令固化到 scripts,让团队统一工作流:

1
2
3
4
5
6
7
8
9
10
{
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun src/index.ts",
"typecheck": "tsc --noEmit",
"build": "bun build src/index.ts --outdir dist --target bun",
"test": "bun test",
"clean": "rm -rf dist"
}
}
  • devbun --watch 监听文件变化自动重启,开发体验顺滑。
  • start:生产环境直接用 Bun 运行 TS 源码,无需预编译。
  • typechecktsc --noEmit 只做类型检查不产出,常用于 CI 与 git hook。
  • buildbun build 打包成单文件,适合发布 CLI 或无运行时依赖的部署。
  • testbun test 内置测试运行器,支持 describe/it/expect 语法,无需安装 Jest。

💡 bun --watch vs bun --hot--watch 在文件变化时重启整个进程,适合大多数场景;--hot 保留模块状态(如内存中的数据),仅重新加载变更的模块,适合需要保持状态的长期运行服务。

调试与 Source Map

Bun 原生支持调试器,无需额外配置 source map。

用 VS Code 调试

.vscode/launch.json 中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "调试 TS (Bun)",
"runtimeExecutable": "bun",
"args": ["src/index.ts"],
"skipFiles": ["<node_internals>/**"]
}
]
}

.ts 源码里打断点即可,VS Code 会通过 Bun 的 inspector 协议直接定位到 TS 源码行。

用 Chrome DevTools

1
2
bun --inspect src/index.ts
# 打开 chrome://inspect,点击 inspect 连接

--inspect 会在启动后等待调试器连接;若想在入口处暂停,使用 --inspect-brk

构建与部署

Bun 的部署模型比传统 Node + tsc 更简洁:生产环境可以直接用 Bun 运行 TS 源码,无需预编译。如果需要更小的镜像或单文件产物,再用 bun build 打包。

直接运行部署

1
bun src/index.ts

部署时只需把源码、package.jsonbun.lock 拷到服务器,执行 bun install 安装依赖后 bun start 即可。Dockerfile 示例:

1
2
3
4
5
6
7
8
9
10
# ---- 单阶段构建(Bun 直接运行 TS,无需编译阶段)----
FROM oven/bun:latest
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
COPY tsconfig.json ./
COPY src ./src
ENV NODE_ENV=production
EXPOSE 3000
CMD ["bun", "src/index.ts"]

oven/bun 是官方镜像,体积远小于 node:22-slim。由于不需要编译阶段,也不需要 typescripttsx 等开发依赖,构建步骤更少、镜像更小。

打包成单文件

发布 CLI 工具或想让产物更便携时,可用 bun build 把整个项目打成一个 JS 文件:

1
bun build src/index.ts --outdir dist --target bun
1
2
3
4
5
# 产出 dist/index.js,可直接运行
bun dist/index.js
# 甚至可以编译成独立可执行文件(Bun 1.1+)
bun build src/index.ts --compile --outfile todo-api
./todo-api

💡 --compile 生成独立可执行文件:Bun 可以把 TS 项目编译成一个独立的二进制文件(内嵌 Bun 运行时),目标机器无需安装 Bun 即可运行。这是发布 CLI 工具的终极方案。

错误处理

Bun.serve 提供了 error 回调,统一处理路由中抛出的异常。配合 TypeScript,可以构建类型安全的错误处理流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/app.ts(接续前文)
import { env } from './config';

export const server = Bun.serve({
// ...routes 同前...

// 未匹配路由的兜底处理
fetch() {
return json(404, { success: false, error: '路由不存在' });
},

// 错误处理:路由中抛出的异常会到这里
error(err) {
console.error('未捕获异常:', err);
const message = env.NODE_ENV === 'production' ? '服务器内部错误' : err.message;
return json(500, { success: false, error: message });
},
});

由于 Bun.serve 的路由处理函数支持 async,路由中 throw 的异常会被自动捕获并转交给 error 回调,无需像 Express 那样手写 asyncHandler 包装器:

1
2
3
4
5
6
7
8
9
10
11
// 路由中直接 throw,会被 error 回调捕获
'/todos': {
POST: async (req) => {
const body = (await req.json()) as CreateTodoInput;
if (!body.title) {
throw new Error('title 必填'); // 自动转交 error 回调
}
const todo = todoRepo.create(body);
return json<Todo>(201, { success: true, data: todo });
},
},

最佳实践速查表

维度建议
类型声明Bun 专属 API 用 @types/bun,Node 兼容模块也由 @types/bun 覆盖
导入前缀统一用 node: 前缀导入 Node 兼容模块(node:fsnode:path
模块系统Bun 推荐 moduleResolution: "bundler",导入路径可省略扩展名
文件操作优先 Bun.file() / fs/promises 异步 API,避免阻塞事件循环
路径拼接一律用 path.joinimport.meta.dir,不要手写 /\
环境变量Bun 自动加载 .env,用 Zod 在启动期校验,导出强类型 env 对象
严格模式开启 strict,额外推荐 noUncheckedIndexedAccess
运行工具开发用 bun --watch,生产用 bun src/index.ts,CI 用 tsc --noEmit
调试bun --inspect,VS Code 用 runtimeExecutable: "bun" 直接调试源码
部署单阶段 Docker(oven/bun 镜像),或 bun build --compile 生成独立二进制
脚本dev / start / typecheck / build / test 五件套
错误处理Bun.serveerror 回调统一捕获,路由中直接 throw 即可
测试bun test 内置测试运行器,无需安装 Jest

类型系统是 Bun 后端项目的护城河:配置校验、路由类型、仓储层契约都在编译期被检查。Bun 把运行时成本降到几乎为零,你只需要在 CI 里跑一次 tsc --noEmit 就能拿到"编译通过即可信"的开发体验。把本篇的脚手架当作新项目的起点,在实践中体会 Bun + TypeScript 带来的工程效率提升。

至此,TypeScript 教程系列完结。从类型基础到工程化,再到服务端实战,希望这套教程能帮助你把 TypeScript 真正用进真实项目。