运行 TypeScript 与 JSX
本教程共 34 篇 · 第 5 篇 · 更新于 2026-08-06
本节目标:
- 理解 Bun 运行 TypeScript「只转译、不检查类型」的底层机制
- 会用
tsconfig.json配置 JSX 转换(react / react-jsx / 自定义工厂)- 使用
paths配置路径别名,告别又长又脆的相对路径- 避开常见坑:扩展名导入、
preserve不被支持、类型错误不报错
上一节我们跑通了第一个 .ts 文件。这一节把 TypeScript 与 JSX 的运行机制讲透,让你不仅「能跑」,还知道「为什么能跑」以及「哪里会绊倒」。
5.1 免编译的本质:转译而非编译
Bun 在加载 .ts / .tsx / .jsx 文件时,会用内置的转译器(transpiler)把源码转换成纯 JavaScript,再交给 JavaScriptCore 执行。这个过程的几个关键事实:
- 剥离类型,不做类型检查:类型标注、接口、泛型等只会被「删掉」,Bun 不会像
tsc那样报错或推断。即使类型写错,只要能转成合法 JS,就能运行。 - 转换 JSX / TSX:
<Component />这类语法会被转成React.createElement(...)或jsx(...)调用。 - 保留语义:变量、函数、模块导出等运行时行为不受影响。
- 速度极快:转译在加载阶段完成,对单文件几乎是毫秒级。
- 有缓存加持:对于大于 50 KB 的源文件,Bun 会把转译结果(及 sourcemap)缓存到磁盘,路径由
BUN_RUNTIME_TRANSPILER_CACHE_PATH控制。缓存按内容寻址、跨项目共享,所以反复运行大型 CLI 时第二次会明显更快。在临时文件系统(如某些容器环境)里把该变量设为0即可关闭缓存。
// 这段代码「类型层面」明显有问题,但 Bun 照样能跑:
const x: number = "hello"; // 类型错误,但运行时只是个字符串赋值
console.log(x.toUpperCase());
Warning转译是「剥离类型」,不是「编译成等价逻辑」,所以 Bun 不做全量类型推导——这是它启动快的原因之一,代价是类型错误被静默忽略。
记住一句话:「Bun 跑起来没报错」不等于「类型正确」。把 Bun 当运行时,把
tsc当类型守门员,角色分清。团队项目里把tsc --noEmit放进 pre-commit 或 CI,作为第二道防线。
5.2 安装 Bun 的类型声明
为了让编辑器(VS Code 等)识别 Bun 全局对象与 Bun.* API 的类型,需要安装官方类型包:
bun add -d @types/bun
之后在 tsconfig.json 的 compilerOptions.types 里加上 "bun",编辑器就不会再对 Bun.serve、Bun.file 等报红了。这一步只影响编辑器提示,不影响 Bun 运行。
推荐给 Bun 项目的 tsconfig.json 选项(运行 bun init 会自动生成):
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"types": ["bun"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true
}
}
其中几个选项的意义:
module: "Preserve"+allowImportingTsExtensions:允许在import里直接写.ts/.tsx扩展名(Bun 原生支持)。moduleResolution: "bundler":使用 bundler 风格的模块解析,配合路径别名更顺手。jsx: "react-jsx":JSX 走 React 17+ 的自动运行时(jsx/jsxDEV),无需手写import React。
5.3 JSX 转换的几种模式
Bun 读取 tsconfig.json(或 jsconfig.json,或 bunfig.toml)来决定 JSX 怎么转。可配置的 jsx 取值:
jsx 取值 | 转换结果 |
|---|---|
"react" | React.createElement(Box, {...}) |
"react-jsx" | import { jsx } from "react/jsx-runtime"; jsx(Box, {...}) |
"react-jsxdev" | 同上但引入 jsxDEV,含开发期校验 |
"preserve" | 不被 Bun 支持,JSX 不会被转译 |
示例:
function Box(props: { width: number }) {
return <div style={{ width: props.width }} />;
}
当 jsx: "react-jsx" 时,Bun 会把它转成从 react/jsx-runtime 引入 jsx 的调用,你不需要手动 import React。
Warning
jsx: "preserve"在 Bun 中不支持。如果你把tsconfig.json设成preserve(通常是为了交给别的打包器再处理),用 Bun 直接运行会得到未转译的 JSX,运行时报错。用 Bun 跑就用react或react-jsx,把「preserve」留给纯构建管线。
针对非 React 的库(如 Preact),可以用 jsxFactory 与 jsxImportSource:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
这样 JSX 会从 preact/jsx-runtime 引入工厂函数。也可以用文件级 pragma 注释临时覆盖:
// @jsxImportSource preact
export function App() {
return <div>hi</div>;
}
5.4 路径别名(tsconfig paths)
长相对路径(../../../../utils/format)既难读又易碎。Bun 会读取 tsconfig.json 的 paths 字段,把别名重写成真实路径:
{
"compilerOptions": {
"paths": {
"my-custom-name": ["./node_modules/zod"],
"@components/*": ["./src/components/*"]
}
}
}
import { z } from "my-custom-name"; // 实际指向 zod
import { Button } from "@components/Button"; // 实际指向 ./src/components/Button
Tip
paths是 Bun 在运行时自动应用的,不需要 webpack / vite 的resolve.alias配置。但要注意:paths的基路径是tsconfig.json所在目录,且通配符*必须成对出现(左侧@components/*、右侧./src/components/*)。写错一侧会导致解析失败。
5.5 直接导入 .ts 扩展名与导入 HTML/资源
Bun 原生支持在 import 里带扩展名:
import { foo } from "./foo.ts"; // 显式写 .ts 也行
这在 moduleResolution: "bundler" 下特别自然,也避免了 Node.js 默认禁止扩展名导入的限制。
更有意思的是,Bun 还能把非 JS 文件当作模块导入。例如全栈开发时导入 HTML:
import index from "./index.html";
Bun.serve({
routes: {
"/": index,
},
});
不同类型的文件会被不同的 loader 处理(HTML、CSS、图片、JSON、TOML、WASM 等),这部分属于打包器与资源加载的进阶内容,后面的章节会展开。
跑 TS/JSX 时,ESM 与 CommonJS 的互操作同样成立。.ts 文件里可以 import 一个 CommonJS 包,CJS 风格的代码里也可以 require 一个 .ts 模块,Bun 在内部做桥接。
这让「渐进式把 .js 改名成 .ts」变得可行:旧文件继续用 CJS,新文件写 ESM,两者共存无碍。
5.6 调试 JSX 的小技巧
Bun 对 JSX 做了特殊日志美化。如果你 console.log 一个 JSX 元素,它会以组件树的形式漂亮地打印出来,方便排查结构问题。此外 Bun 支持「属性简写(prop punning)」:
function Div(props: { className: string }) {
const { className } = props;
return <div {className} />; // 等同于 <div className={className} />
}
5.7 一个完整可运行示例
把前面几点串起来:用 TS 写一个返回 JSX 的函数,配好 tsconfig.json,直接 bun run:
// app.tsx
function Page(props: { title: string }) {
return (
<html>
<body>
<h1>{props.title}</h1>
</body>
</html>
);
}
console.log(<Page title="Hello Bun" />);
tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"types": ["bun"]
}
}
运行:
bun run app.tsx
Bun 会把 JSX 转成 react/jsx-runtime 的调用并打印组件树(前提是项目里装了 React;若只是想在 Node 风格里看结构,也可把 jsx 设为 "react" 并安装 React)。这个例子说明:TS 类型、tsx 扩展名、JSX 语法三者可以「零配置」一起跑,正是 Bun 想给你的体验。
Tip首次在 TSX 里用 React 相关语法时若报「找不到模块 react」,执行
bun add react react-dom即可。Bun 会顺手处理依赖安装,不需要额外的包管理器切换。
5.8 常见坑与排查清单
- 类型错误不报错:这是预期行为,Bun 不检查类型。需要类型安全就在 CI 跑
tsc --noEmit。 jsx: "preserve"运行报语法错:改成react或react-jsx。- 编辑器报
Bun未定义:安装@types/bun并在tsconfig.json加"types": ["bun"]。 - 路径别名不生效:检查
tsconfig.json是否在项目根目录、paths通配符是否左右成对。别名的基路径是tsconfig.json所在目录,从子目录里启动脚本时容易踩空。 - 想用顶层
await:Bun 默认 ESM,顶层await直接可用,无需async包装。 - 导入带
.ts扩展名失败:确认tsconfig.json的allowImportingTsExtensions与moduleResolution: "bundler"已设置,且文件确实以.ts存在。
Note如果你只是想「写 TS 并运行」,记住三件事就够:Bun 会自动转译、类型错误不会拦你、
tsconfig.json主要给编辑器与 JSX/路径别名用。其余的高级compilerOptions属于锦上添花,用到再查即可。
5.9 小结
- Bun 用内置转译器在加载阶段把 TS/JSX 变成 JS,只转译、不检查类型。
- 类型声明用
@types/bun+tsconfig.json的types: ["bun"]解决编辑器提示。 - JSX 转换通过
tsconfig.json的jsx/jsxImportSource控制;preserve不被 Bun 支持。 paths字段可做路径别名,运行时自动重写,告别长相对路径。- 类型安全、扩展名导入、preserve 不支持是三个最典型的坑,按清单排查即可。