| | |

Bun 07 ⚡ Running JavaScript, TypeScript, and JSX Files Natively Without Transpilation

⏱️ Estimated read: 9 min | |

Bun’s runtime is built around a full transpiler, not a type-stripper. When you run a .ts, .tsx, or .jsx file with bun run, the engine reads the source, applies the JSX transform specified in tsconfig.json (or bunfig.toml), strips the TypeScript type annotations, and executes the resulting JavaScript directly . There is no separate tsc step, no esbuild watch process, and no ts-node wrapper. The trade-off is that Bun does not type-check during execution—tsc --noEmit or bun check remains a separate CI step .

Key point: Bun’s runtime transpiler supports the full TypeScript syntax surface, including enum, namespace with runtime behavior, and decorators. Node.js 24+ native TypeScript support is type-stripping only, which rejects these constructs . The runtime reads tsconfig.json for paths, jsx, and jsxImportSource, so the editor and the runtime agree on module resolution .


Why native execution differs from type-stripping

The full transpile problem. Node.js 24 introduced native TypeScript support, but the implementation is deliberately minimal: it erases type annotations and nothing else. Syntax that requires code generation—enum, namespace with runtime values, parameter properties in constructors, and decorators—produces ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX . Bun’s transpiler is closer to what tsc emits. It handles these constructs because it generates JavaScript, not just strips annotations.

The JSX problem. Node.js does not natively support .tsx or .jsx files. A build step is required. Bun’s internal transpiler converts JSX into vanilla JavaScript before execution, using the transform specified in tsconfig.json . The jsx compiler option controls which factory function is used: react emits React.createElement, react-jsx imports from react/jsx-runtime, and react-jsxdev uses the development runtime with additional validation checks . For Preact or other libraries, jsxFactory and jsxImportSource override the default React behavior.

The tsconfig.json problem. Bun reads tsconfig.json at runtime, not just at build time. The paths mapping is honored, so an import like @utils/helpers resolves to the correct file without a bundler plugin. The jsx options are read, so the runtime and the editor agree on how JSX is transformed. The moduleResolution setting should be bundler and allowImportingTsExtensions should be true for Bun projects .

The type-checking problem. Bun does not type-check. A file with type errors runs successfully. The bun check command runs the TypeScript compiler in check-only mode, and it behaves like TypeScript 7 regardless of which typescript version is installed . The @types/bun package provides the type definitions for Bun’s built-in APIs, and since TypeScript 6.0, the types array in tsconfig.json must explicitly include "bun" .

The trade-off. Running TypeScript directly is faster because there is no separate build step, but it means type errors are not caught at execution time. The CI pipeline must run bun check or tsc --noEmit separately. The runtime is a transpiler, not a verifier.


a. Running TypeScript and JSX files directly

The bun run command accepts a file path and executes it. The file extension determines the loader: .ts uses the TypeScript loader, .tsx uses the TypeScript + JSX loader, .jsx uses the JSX loader, and .js uses the JavaScript loader .

# Run a TypeScript file
bun run src/server.ts

# Run a TSX file (React component with TypeScript)
bun run src/App.tsx

# Run a JSX file
bun run src/component.jsx

The transpilation happens in memory. No .js file is written to disk unless bun build is used. The execution is immediate: the source is parsed, the types and JSX are transformed, and the resulting code runs in JavaScriptCore.

The --watch flag restarts the process on file changes. The --hot flag enables hot reload, which preserves state between reloads for supported frameworks.

# Watch mode for a TypeScript file
bun --watch run src/server.ts

b. Configuring JSX transforms in tsconfig.json

Bun reads tsconfig.json to determine how JSX should be transformed. The jsx option is the primary setting, and its value determines which factory function is used .

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react",
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "types": ["bun"]
  }
}

The jsx: "react-jsx" setting uses the automatic runtime, which imports jsx from react/jsx-runtime. The jsx: "react" setting uses the classic runtime, which calls React.createElement. For Preact, jsxImportSource: "preact" changes the import source to preact/jsx-runtime .

The jsxFactory and jsxFragmentFactory options override the function names when jsx is set to react. This is the legacy mechanism for libraries like Preact that use h instead of React.createElement .

The bunfig.toml file can also configure JSX options, which is useful when a tsconfig.json is not present or when the settings need to differ from the TypeScript configuration.

# bunfig.toml
[jsx]
runtime = "automatic"
importSource = "react"

c. Type checking and @types/bun

Bun does not type-check. The bun check command runs the TypeScript compiler in check-only mode. Since TypeScript 6.0, the types array in compilerOptions defaults to an empty array, so @types/bun must be explicitly included .

# Install type definitions
bun add -d @types/bun

# Type check the project
bun check

The tsconfig.json must include "types": ["bun"] for the Bun global to be recognized in the editor. Without this, TypeScript reports Cannot find name 'Bun' . The @types/bun package provides definitions for Bun.serve, bun:sqlite, Bun.file, and other built-in APIs.

For projects that use TypeScript 6 or 7, the full recommended compilerOptions includes module: "Preserve", target: "ESNext", moduleResolution: "bundler", and allowImportingTsExtensions: true .


Complete Example Session

# ============================================
# PART 1: RUN A TYPESCRIPT FILE
# ============================================

echo 'const msg: string = "Hello"; console.log(msg);' > hello.ts
bun run hello.ts
# Output: Hello
# ============================================
# PART 2: RUN A TSX FILE
# ============================================

cat > App.tsx <<'EOF'
function App() { return <h1>Hello</h1>; }
console.log(<App />);
EOF

bun run App.tsx
# Output: { type: 'h1', props: { children: 'Hello' }, ... }
# ============================================
# PART 3: CONFIGURE JSX TRANSFORM
# ============================================

cat > tsconfig.json <<'EOF'
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "types": ["bun"]
  }
}
EOF
# ============================================
# PART 4: INSTALL TYPE DEFINITIONS
# ============================================

bun add -d @types/bun
# @types/bun installed as devDependency.
# ============================================
# PART 5: TYPE CHECK
# ============================================

bun check
# Runs TypeScript in check-only mode.
# Reports type errors without executing.
# ============================================
# PART 6: RUN WITH WATCH MODE
# ============================================

bun --watch run hello.ts
# Restarts on file changes.
# ============================================
# PART 7: VERIFY FULL TS SYNTAX
# ============================================

cat > enum.ts <<'EOF'
enum Direction { Up, Down }
console.log(Direction.Up);
EOF

bun run enum.ts
# Output: 0
# enum is supported, unlike Node's type-stripping.
# ============================================
# PART 8: USE TSCONFIG PATHS
# ============================================

# tsconfig.json:
# "paths": { "@utils/*": ["./src/utils/*"] }
bun run src/server.ts
# Imports using @utils/ resolve at runtime.
# ============================================
# PART 9: COMPARE WITH NODE.JS
# ============================================

node hello.ts
# Node 24: works (type-stripping).
node enum.ts
# Node 24: ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX.

The nine parts trace the runtime execution: TypeScript file, TSX file, JSX configuration, type definitions, type checking, watch mode, enum support, tsconfig.json paths, and the comparison with Node.js.


Quick Reference

File Extensions and Loaders

ExtensionLoaderTransforms
.tsTypeScriptTypes removed
.tsxTypeScript + JSXTypes removed, JSX transformed
.jsxJSXJSX transformed
.jsJavaScriptNo transform

JSX Compiler Options

OptionValueEffect
jsxreactReact.createElement
jsxreact-jsxjsx from jsx-runtime
jsxreact-jsxdevjsxDEV with validation
jsxFactoryhCustom factory (Preact)
jsxImportSourcepreactCustom import source

Type Checking

CommandPurpose
bun checkRun TypeScript check-only
bun add -d @types/bunInstall Bun type definitions
"types": ["bun"]Required for TypeScript 6+

Best Practices

✅ Do This:

bun run src/server.ts
<!-- Run TypeScript directly without a build step -->   # ✅
{ "compilerOptions": { "jsx": "react-jsx", "types": ["bun"] } }
<!-- Configure JSX and Bun types -->                  # ✅
bun check
<!-- Run type checking separately in CI -->           # ✅

❌ Don’t Do This:

# Assume Bun type-checks during execution
<!-- It does not; use bun check -->                   # ❌
# Rely on Node.js type-stripping for enum or decorators
<!-- Node 24 rejects them; Bun supports them -->      # ❌
# Omit "types": ["bun"] with TypeScript 6+
<!-- Bun global is not recognized -->                 # ❌

Common Pitfalls

PitfallWhy It HappensFix
Type errors not caughtBun does not type-checkRun bun check
Bun global not recognizedMissing "types": ["bun"]Add to tsconfig.json
JSX transform wrongjsx option not setConfigure in tsconfig.json
tsconfig.json paths ignoredmoduleResolution not bundlerSet moduleResolution: "bundler"
Decorators failExperimental decorators not enabledBun supports TC39 decorators by default

Real-World Examples

1. Run TypeScript

bun run index.ts

2. Run TSX

bun run App.tsx

3. Configure JSX

{ "compilerOptions": { "jsx": "react-jsx" } }

4. Install Types

bun add -d @types/bun

5. Type Check

bun check

6. Watch Mode

bun --watch run server.ts

7. Enum Support

bun run enum.ts

8. tsconfig Paths

{ "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }

9. Preact JSX

{ "jsx": "react-jsx", "jsxImportSource": "preact" }

10. Node Comparison

node enum.ts
# ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX

Visual

Runtime Transpilation Flow

┌──────────────────────────────────────────────┐
│  bun run App.tsx                             │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Read tsconfig.json                          │
│  ├── jsx: react-jsx                          │
│  ├── jsxImportSource: react                  │
│  └── paths: @/*                              │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Bun transpiler                              │
│  ├── Strip TypeScript types                  │
│  ├── Transform JSX → jsx() calls             │
│  └── Resolve paths imports                   │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Execute in JavaScriptCore                   │
└──────────────────────────────────────────────┘

JSX Transform Options

┌──────────────────────────────────────────────┐
│  jsx: "react"                                │
│  → React.createElement(Box, { width: 5 })    │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  jsx: "react-jsx"                            │
│  → import { jsx } from "react/jsx-runtime"   │
│  → jsx("Box", { width: 5 })                  │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  jsx: "react-jsx" + jsxImportSource: "preact"│
│  → import { jsx } from "preact/jsx-runtime"  │
└──────────────────────────────────────────────┘

Summary

ItemValue
TypeScript executionNative, full transpile
JSX executionNative, configurable transform
tsconfig.jsonRead at runtime
Type checkingSeparate (bun check)
@types/bunRequired for Bun global
types arrayRequired for TypeScript 6+
Enum supportYes (unlike Node type-stripping)
DecoratorsTC39 decorators supported

Key takeaways:

  • Bun transpiles, Node strips. Bun’s runtime generates JavaScript from TypeScript and JSX. Node.js 24’s native TypeScript support only erases types, rejecting enum, namespace with runtime behavior, and decorators .
  • tsconfig.json is read at runtime. The jsx, jsxImportSource, and paths options affect execution, not just the editor. The runtime and the type checker agree on module resolution .
  • Type checking is separate. Bun does not type-check during execution. bun check runs the TypeScript compiler in check-only mode, and it must be part of the CI pipeline .
  • @types/bun is required for the Bun global. Since TypeScript 6.0, the types array defaults to empty, so "types": ["bun"] must be added explicitly .
  • JSX transform is configurable. The jsx option selects between the classic runtime (React.createElement) and the automatic runtime (jsx from jsx-runtime). Preact and other libraries override the import source .

Remember: Bun runs TypeScript and JSX because it has a transpiler built into the runtime. The transpiler reads tsconfig.json, transforms the source in memory, and executes the result. It does not verify types. The bun check command is the verifier. The @types/bun package is the type definition provider. The runtime is fast because it skips the type-checking step; the type-checking step is still necessary for correctness.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!