首页 / Bun 入门教程 / 运行 TypeScript 与 JSX

Bun 入门教程

运行 TypeScript 与 JSX

本教程共 34 篇 · 第 5 篇 · 更新于 2026-08-06

BunTypeScriptJSXTSXtsconfig路径别名转译

本节目标:

  • 理解 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.jsoncompilerOptions.types 里加上 "bun",编辑器就不会再对 Bun.serveBun.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 跑就用 reactreact-jsx,把「preserve」留给纯构建管线。

针对非 React 的库(如 Preact),可以用 jsxFactoryjsxImportSource

{
  "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.jsonpaths 字段,把别名重写成真实路径:

{
  "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" 运行报语法错:改成 reactreact-jsx
  • 编辑器报 Bun 未定义:安装 @types/bun 并在 tsconfig.json"types": ["bun"]
  • 路径别名不生效:检查 tsconfig.json 是否在项目根目录、paths 通配符是否左右成对。别名的基路径是 tsconfig.json 所在目录,从子目录里启动脚本时容易踩空。
  • 想用顶层 await:Bun 默认 ESM,顶层 await 直接可用,无需 async 包装。
  • 导入带 .ts 扩展名失败:确认 tsconfig.jsonallowImportingTsExtensionsmoduleResolution: "bundler" 已设置,且文件确实以 .ts 存在。
Note

如果你只是想「写 TS 并运行」,记住三件事就够:Bun 会自动转译、类型错误不会拦你、tsconfig.json 主要给编辑器与 JSX/路径别名用。其余的高级 compilerOptions 属于锦上添花,用到再查即可。

5.9 小结

  • Bun 用内置转译器在加载阶段把 TS/JSX 变成 JS,只转译、不检查类型
  • 类型声明用 @types/bun + tsconfig.jsontypes: ["bun"] 解决编辑器提示。
  • JSX 转换通过 tsconfig.jsonjsx / jsxImportSource 控制;preserve 不被 Bun 支持。
  • paths 字段可做路径别名,运行时自动重写,告别长相对路径。
  • 类型安全、扩展名导入、preserve 不支持是三个最典型的坑,按清单排查即可。