Published on npm as
typescript-native-bridge.
A drop-in typescript replacement that type-checks on Go. Swap the typescript
package for this fork and keep using tsc, vue-tsc, svelte-check, astro-check,
glint, ESLint, and your editor exactly as before — the checker runs on tsgo
(Microsoft's Go TypeScript compiler) in-process instead of JavaScript. No new CLI, no
new LSP, no per-tool config, no code changes.
typescript@7 is Microsoft's Go-native rewrite — but it doesn't drop into the tools you
actually use:
vue-tsc/astro-check/svelte-check/glintare built on the classictypescriptprogrammatic API (createProgram, Volar hooks, custom hosts). v7's programmatic surface is the new tsgo API — not a drop-in replacement for the classic one, so those tools can't just move to it.- ESLint (typescript-eslint) imports the classic
typescriptAPI and callsgetTypeChecker()— same API mismatch. - Editors run
tsserver+ Language Service Plugins (@vue/typescript-pluginfor.vue) — tsgo's LSP doesn't support that plugin model.
TNB keeps the classic package surface and puts the v7 engine (tsgo 7.x) behind it
in-process — so one typescript override accelerates all of them at once.
# pnpm-workspace.yaml
overrides:
typescript: npm:typescript-native-bridge@<version>pnpm install
pnpm exec vue-tsc -b --noEmit # or your project's typecheck scriptIf packages depend on typescript via catalog:, update the catalog entry too,
or those packages still resolve stock TypeScript:
catalog:
typescript: npm:typescript-native-bridge@<version>
overrides:
typescript: npm:typescript-native-bridge@<version>Use the alias and the $typescript override reference as shown — putting
npm:typescript-native-bridge@… directly inside overrides is rejected or mis-resolved
by some npm versions (issue #8). <version> is an exact version
(e.g. 6.0.3-bridge.6.tsgo.7.0.2 — pin exactly; caret ranges don't match
prerelease versions) or the latest dist-tag.
// package.json
{
"resolutions": {
"typescript": "npm:typescript-native-bridge@<version>"
}
}# pnpm-workspace.yaml
overrides:
typescript: link:../typescript-native-bridgeThe checkout must be built first (requires Go — npm run setup in the TNB repo).
After any override change: reinstall. The override applies repo-wide — vue-tsc,
@typescript-eslint/parser, and every other transitive typescript consumer picks up
the fork.
On the first type-check in a process, TNB prints to stderr:
┌─────────────────────────────────────────────────────────┐
│ ✅ TNB ACTIVE — `typescript` is the tsgo-backed fork │
└─────────────────────────────────────────────────────────┘
No banner = stock typescript is still loaded. See Troubleshooting.
node -e "console.log(require.resolve('typescript'))"
# should point at typescript-native-bridge, not node_modules/typescript@6.xVerified means: the tool runs on the fork and its behavior matches tsgo's on the stated workload (no crash, no silent under-reporting, no false positives beyond the differences from tsgo).
| Tool | Status | Verified on |
|---|---|---|
tsc |
✅ | compiler test corpus |
vue-tsc |
✅ | elk.zone monorepo (~2,000 files): emitted-error parity with stock, ~3.1× faster |
astro-check |
✅ | fixture project: output identical to stock |
svelte-check |
✅ | fixture project: output identical to stock (incl. svelteHTML ambient shims) |
glint |
✅ | fixture project: same error set as stock (transformed .gts virtual files) |
| ESLint + typescript-eslint (type-aware rules) | ✅ | 1,000-file type-aware corpus: lint output byte-identical to stock |
tsserver + @vue/typescript-plugin |
✅ | volar language-tools test suite: 205/209 pass (4 skipped) |
tsslint |
✅ | runs as the volar repo's own linter |
Continuous verification: a nightly CI gate replays the language-service probe
corpus (quickinfo / definition / references / diagnostics, ~19k units) against the
same stock build — no new divergences allowed. If your tool isn't listed, try it and
file an issue; the fork covers any tool that drives the standard typescript
Compiler API.
.vue,.svelte,.astro,.mdx,.gtsetc. via the standardextraFileExtensionscontract — no hard-coded per-framework special case.- Host-injected virtual content (Volar virtual TS, glint's transformed modules, svelte's ambient shims) reaches the Go checker.
allowArbitraryExtensionsis inferredtruewhen host extra extensions are present and tsconfig leaves it unset; explicitfalseopts out.- Not supported: custom
resolveModuleNames/resolveModuleNameLiteralsthat remap an import to a different physical file (the bridge is synchronous JS→Go; tsgo cannot call back into JS resolvers).
Measured on this repo's benchmarks (Apple Silicon; your repo will differ — measure):
| Workload | Stock typescript |
TNB | |
|---|---|---|---|
vue-tsc -b full check (elk.zone, ~2000 files) |
9.7s | 3.1s | ~3.1× |
| type-aware ESLint, single-run (plain TS, one program) | 7.6s | 7.7s | parity |
| JS heap peak (1000-file type-aware ESLint run) | 1.57GB | 0.90GB | −43% |
The rule is simple: wherever the time is in the checker, TNB is faster. The Go engine does the whole-program semantic pass ~10× faster than the JS checker — the question for any workload is how much of its time that phase is.
Editor / LS path (Volar + tsserver): the V8-arena transport (fixed-layout records written straight into V8 memory) plus a binary blob codec for the auto-import export map keep per-keystroke work near the transport floor. Measured on a 5,537-request roam over the volar corpus: completionInfo 0.3ms mean with 290 bytes and 1.0 RPC per request; quickinfo 0.5ms; references 4.8ms; total JSON transport 7.7MB.
vue-tsc -b (checker-dominated — the big win):
| Stock | TNB | |
|---|---|---|
| Type-checking | ~5.6s (JS checker) | ~0.6s (Go checker) |
| Tool overhead (Volar codegen, JS, transport) | ~4.1s | ~2.5s |
| Total | 9.7s | 3.1s |
The single-run ESLint row is the control case: its type-checking share is small (the tool spends its time on parsing, AST conversion and rule execution — work both sides pay), so the total lands at parity. Workloads dominated by transport volume rather than checker work (e.g. watch-mode ESLint issuing ~469K tiny queries — one program rebuild per linted file) measure the JS↔Go boundary, not the engine; the V8-arena transport (fixed-layout records written straight into V8 memory, DataView reads, interned strings) keeps that boundary near its floor, and memory stays well below stock either way.
CLI typecheck picks up TNB automatically. The editor does not — VS Code ships its own TypeScript and only uses yours when you opt in.
1. Workspace settings (commit .vscode/settings.json for the team):
{
"js/ts.tsdk.path": "node_modules/typescript/lib",
"js/ts.tsdk.promptToUseWorkspaceVersion": true
}Use a path relative to the workspace folder that contains node_modules.
2. Switch to the workspace version (once per machine):
Command Palette → TypeScript: Select TypeScript Version → Use Workspace Version.
3. Verify: the version picker shows a path under node_modules/typescript/lib; the
Output → TypeScript channel may show TNB ACTIVE on first project load. Vue/Nuxt users:
keep @vue/typescript-plugin in tsconfig compilerOptions.plugins as today — it runs
as a tsserver LS Plugin on this fork.
| CLI | Editor | |
|---|---|---|
| Override needed | Yes | Yes (same node_modules/typescript) |
| Extra config | No | js/ts.tsdk.path + Use Workspace Version |
The checker's behavior is tsgo 7.0.2's (Microsoft's Go TypeScript), not stock TypeScript 6.0.3's — migrating from stock means inheriting tsgo's diagnostics, bundled libs, and display output as-is.
TNB's own changes to tsgo behavior — the complete list, enforced by CI:
| Change | Why | Upstream | Removal |
|---|---|---|---|
getTypeFromTypeNodeWorker resolves type-position entity names (identifier / qualified name / property access) |
Silent wrong result on the headline path: hover on P in [P, (typeof OBJ)[P][number]] read any instead of the type parameter (issue #30) |
repro branch repro/type-position-entity-reads-any (issue pending) |
When the upstream fix lands |
Anything that looks like a difference from stock 6.0.3 but isn't listed here is tsgo's own behavior, not TNB's. Found an actual TNB-only divergence? File an issue with a minimal repro.
The bridge binary ships as per-platform optional dependencies; npm install pulls only
the one matching your machine (the main package is pure JS):
| Platform | Sub-package |
|---|---|
| macOS Apple Silicon | @typescript-native-bridge/darwin-arm64 |
| macOS Intel | @typescript-native-bridge/darwin-x64 |
| Linux x64 | @typescript-native-bridge/linux-x64 |
| Linux arm64 | @typescript-native-bridge/linux-arm64 |
| Linux arm (32-bit) | @typescript-native-bridge/linux-arm |
| Windows x64 | @typescript-native-bridge/win32-x64 |
| Windows arm64 | @typescript-native-bridge/win32-arm64 |
Linux packages target glibc 2.31 and are rejected by the release gate if they
acquire a newer symbol requirement. Alpine/musl is not supported: Go's
-buildmode=c-shared runtime crashes at load on musl libc — even a trivial
hello-world c-shared library segfaults, on Go 1.22 through 1.26 alike
(golang/go#13492, a 10-year-open
upstream issue with an active fix in
golang/go#75048; tsgo's own CLI
works on Alpine only because it ships CGO-free static binaries, and a NAPI
bridge cannot be CGO-free). Workaround: run the typecheck/lint step in a
glibc-based image (node:24 or node:24-bookworm-slim) and deploy into the
Alpine stage of your multi-stage build; apk add gcompat does not help.
On an unsupported platform the loader fails with a clear "unsupported platform or
missing optional dependency" error — build from source there (clone with submodules,
then npm run setup; requires Go + a C toolchain).
| Check | Action |
|---|---|
| Override at workspace root | Monorepo: pnpm-workspace.yaml, not a leaf package |
| pnpm 11 | Move package.json → pnpm.overrides to pnpm-workspace.yaml → overrides: (pnpm 11 no longer reads the pnpm field — silently ignored) |
catalog: pin |
Update catalog and overrides |
| Stale install | pnpm install again; clear CI cache if needed |
| Wrong resolution | node -e "console.log(require.resolve('typescript'))" |
- CLI OK, editor not: add the tsdk settings and run TypeScript: Select TypeScript Version → Use Workspace Version. The override alone is not enough for the editor.
- Editor OK, CLI not: check
require.resolve('typescript')— should point at TNB. Reinstall after changing overrides.
Expected — the checker's behavior is tsgo 7.0.2's, not stock 6.0.3's, so output can differ from stock (see Behavior and differences from tsgo). What is a bug: output that differs from tsgo itself — file an issue with a minimal repro.
Error mentioning bridge.dylib / bridge.so / bridge.dll / "unsupported platform" →
see Platform support (build from source, or use a link: install
built with npm run setup).
TSGO_PROFILE=1 prints a [tsgo-profile] RPC/timing summary to stderr on process exit.
Remove the typescript override, reinstall, confirm:
pnpm install
node -e "console.log(require.resolve('typescript'))" # stock typescript@6.x againNo source changes required.
Do I need to change my code? No.
Do I configure vue-tsc / ESLint / my editor plugin separately? No. They import
typescript; one override covers them.
Is this the same as TypeScript 7 / tsgo? Same engine, different package. TNB pins
tsgo 7.x as its checker (the version string ends in tsgo.7.0.2), but keeps the classic
typescript API and tsserver in front of it. typescript@7 gives you the new tsgo
API and its own LSP instead — see Why not just use TypeScript 7?
How much faster is it? See Performance — biggest on vue-tsc-style
full-program workloads. Measure on your own repo.
Apache-2.0. See LICENSE and NOTICE.
This package is a derivative work of Microsoft TypeScript and microsoft/typescript-go.