notes to self

Run two versions of the same npm package⁠

On this page

We're going to run two versions of the same npm package simultaneously, with an iterative upgrade to TypeScript 7 as the working example.

TypeScript 7's compiler is a Go binary, around 10x faster on paper, and (currently) at RC. I want that speed in CI today, but most of the ecosystem isn't ready yet. ts-jest, Next's build, and tsserver all need TypeScript's programmatic API, which won't ship until 7.1, so replacing the stable package outright would break existing connections.

Instead of replacing, we'll run two versions of the package side by side. The new one scoped to a single task, with the old package still in place for everything else. If it goes wrong, it's an easy revert, and we can get back to crying about our slow pipelines.

Swap the binary, keep the API⁠

Build-pipeline tools (compilers, linters, bundlers) usually ship in two parts. In TypeScript's case:

We can swap the binary without touching the API by using a shim. If your tsconfigs set noEmit: true, the tsc binary only does type-checking, and type-checking is good to go on 7, so that's our single task, which luckily for us, gives us the biggest speed gains.

Install and pin the new package⁠

Install the RC alongside stable, under an alias, pinned exact:

"typescript7": "npm:typescript@7.0.1-rc"

Build the shim bin⁠

Give it a bin of its own. Both packages declare a tsc binary, but only one can own .bin/tsc (which depends on your package manager and install order). A shim with a name of its own avoids any conflicts:

#!/usr/bin/env node
// bin/tsc7.mjs (the new freezer, plugged in next to the old one)
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";

const tsc = fileURLToPath(import.meta.resolve("typescript7/bin/tsc"));
const result = spawnSync(tsc, process.argv.slice(2), { stdio: "inherit" });
process.exit(result.status ?? 1);

Update your package.json⁠

Then declare it in the package.json bin field, so the next install links it as .bin/tsc7:

"bin": {
  "tsc7": "./bin/tsc7.mjs"
}

The alias, the shim, and the bin entry all live in one shared config package that every workspace already depends on. Install once, and tsc7 is on the PATH everywhere.

Use it selectively⁠

Point the type checker scripts at it:

"check-types": "tsc7 --noEmit"

Tests, builds, and editors keep resolving stable TypeScript, and only check-types runs on 7.

Shim it, bin(ary) it, then bin it again⁠

When 7.1 ships and the ecosystem catches up, we can delete the alias and the shim, and point the scripts back at plain tsc. The shim is a small, temporary addition which brings the new tool in safely, and gets binned once we can fully replace the old package with the new.