September 8, 2023·6 min read·engineeringtypescriptbunruntimetooling

Bun 1.0 integrates TypeScript execution with the rest of the toolchain

Bun combines runtime transpilation, package installation, bundling and tests. Its first stable release reduces setup while leaving static checking and deployment targets as separate decisions.

Running a TypeScript program often requires arranging a runtime and a separate transformation step before the program can begin. Tests and browser builds can add their own transformations of the same source. Bun 1.0 brings those operations into one toolkit, with a transpiler inside the runtime and shared support for TypeScript and JSX.

The September 8 announcement presents Bun's first stable release as an integrated runtime, package manager, bundler and test runner. Its release-tag TypeScript documentation makes a necessary distinction explicit. Bun transpiles TypeScript into JavaScript for execution, but does not type-check it. A project can continue using tsc for static analysis.

I am interested in that integration because TypeScript's surrounding web ecosystem is the main reason I use it. Reducing the machinery needed to execute a project is useful if it preserves the checks and framework behavior the project depends on. A single executable changes where those responsibilities are implemented without making them identical.

Direct execution still includes transformation

Bun can run a .ts or .tsx file directly. Its runtime recognizes the source form, transforms it internally and executes the resulting JavaScript. Imported TypeScript files receive the same treatment, including packages exposing TypeScript source.

bun index.ts

The command removes the need to produce a separate JavaScript file merely to start that program under Bun. It does not mean a JavaScript engine executes type annotations as part of ordinary program semantics. The runtime performs the translation before execution.

That distinction explains why static checking remains separate. Executing one path through a program does not establish that all assignments and calls satisfy its declared types. Bun's transformation makes the source runnable, while tsc can analyze the type relationships without producing the runtime artifact.

The versioned documentation recommends noEmit for a TypeScript 5 configuration using bundler resolution. This gives tsc a checking role while Bun handles execution. Application authors can keep static analysis as a required check without using tsc to emit every file they run locally.

The same documentation names bun-types as the package providing Bun's declarations at this release. Those declarations describe host APIs to the checker. They do not cause another runtime to implement Bun-specific facilities, so using them establishes an execution-environment dependency that needs to match deployment.

Source imports and path maps belong to the host

Bun's TypeScript documentation says its runtime respects paths mappings from tsconfig. An alias can therefore resolve during execution as well as during checking. That addresses a common split where the type checker understands an import but another loader requires an additional adapter.

The convenience is specific to the configured host. A path mapping is not automatically part of the JavaScript module semantics supported by every runtime. Code moved to a different execution arrangement still needs a loader or build step that implements its chosen mapping.

The documentation likewise distinguishes running production code under Bun from using Bun only during development. A project targeting Node or browsers still needs appropriate transformation for that target. Executing the source locally does not establish that a browser can load it unchanged.

This is especially relevant to TSX. Parsing JSX and transforming it can make a module executable, but rendering still depends on the library and environment used by the application. Runtime transpilation removes a setup step without supplying every framework behavior around that source.

Package installation is independently usable

The 1.0 package-manager documentation describes bun install as usable in Node projects as well as Bun projects. It reads package.json, installs dependencies into node_modules and writes a binary bun.lockb file. Selecting the installer therefore does not require selecting the runtime for every application process.

This makes adoption divisible. A project can investigate installation behavior while retaining another runtime, or use Bun execution while retaining other parts of its existing build. Integration provides common tools, but the project still chooses which operations those tools own.

Installation behavior also contains policy differences. The tagged documentation says dependency lifecycle scripts are not executed for security reasons, while the project's own install scripts run at their appropriate times. Packages depending on installation-time work need to be understood in terms of that policy.

This is not an incidental implementation detail. A dependency can be present on disk while still requiring a generated or native artifact normally produced by a script. An installer that restricts script execution changes which code may run during dependency acquisition, with consequences for both security and package readiness.

I prefer making that policy visible to describing every package manager as interchangeable because it reads the same manifest. Package format compatibility and lifecycle execution are different contracts. Knowing the distinction helps explain failures without attributing them to TypeScript source execution.

Tests execute in the selected runtime

The release-tag test runner documentation provides a Jest-like API through bun:test. It supports TypeScript and JSX, lifecycle hooks and snapshots. Tests execute using the Bun runtime, so the same host behavior used for direct execution is available during the test.

This avoids requiring a separate TypeScript transformer solely for test loading. It also means that a successful run establishes behavior under Bun. If deployment uses another runtime, differences in host APIs remain relevant even when test syntax resembles an existing Jest suite.

The runner searches for conventional test and spec filename patterns. File-path filters and test-name filters provide different ways to select work. A failed test produces a nonzero process exit code, giving automation a direct success or failure signal.

The versioned documentation says the runner executes tests in one process after loading preload scripts. Shared initialization and cleanup therefore need to respect that execution arrangement. Moving a suite from another runner can change more than the import name used for assertions.

The important compatibility question concerns the particular test facilities a suite uses. Familiar names reduce editing work, but lifecycle behavior, mocking and runtime integration still form the operational contract. An integrated runner is useful when it can execute the tests that establish the application's intended behavior.

Browser bundling remains a separate output operation

The 1.0 bundler documentation describes bun build and the Bun.build JavaScript API. Entry points identify the source graph to process, and the output directory identifies where generated bundles are written. The resulting files are artifacts for another environment to consume.

Bundling can combine dependency files and transform TypeScript or JSX. This is different from starting a server-side source file under Bun. A browser application needs suitable output and the browser APIs its code uses, even when the build tool and development runtime share one executable.

The tagged documentation also records a limit on syntax transformation. Its bundler does not yet attempt general down-conversion of recent ECMAScript syntax. Producing a bundle therefore does not guarantee compatibility with an older JavaScript engine merely because the source was successfully processed.

The JavaScript build API returns a result containing success, output artifacts and logs. The documentation says invalid options can throw, while build failure must be determined from the result. A build wrapper that only catches exceptions can therefore miss an unsuccessful bundle.

That is another place where shorter setup still requires a precise API understanding. The output result is the contract consumed by deployment automation. Checking it is part of making an integrated build useful, just as retaining type checking is part of making direct TypeScript execution useful.

Node compatibility is a concrete dependency question

The announcement emphasizes existing npm packages and Node interfaces, while also acknowledging that perfect compatibility is not possible. Bun uses a different JavaScript engine, and its native APIs provide another way to perform operations such as serving HTTP or reading files.

A project using standard Node interfaces and a project using Bun-native APIs consequently make different portability commitments. The former depends on compatibility behavior for those operations. The latter depends directly on Bun's implementation and declarations.

The useful effect of 1.0 is that these choices can be made within a coherent toolkit. Source transformation, dependency installation and testing no longer require separate integrations merely to begin. Their contracts remain distinct, so a project can preserve static checks and choose its production host while deciding which portions of the integrated workflow it wants to adopt.