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 one dimmed line 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× 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) |
mdx-tsc |
✅ | fixture project: diagnostic output identical to stock (Volar runTsc, errors mapped to MDX source spans) |
| 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, ~2,000 files) |
9.7s | 3.2s | ~3× |
| type-aware ESLint, single-run (1,000 plain-TS files, one program) | 2.3s | 2.4s | +1.5% |
| same, 3,000 files | 6.9s | 6.8s | ~parity |
| JS heap peak (1,000-file ESLint fixture) | 769MB | 631MB | −18% |
| peak RSS, whole-process (vue-tsc -b; TNB's includes the in-process Go checker) | 1.8GB | 3.3GB | ~1.9× — structural |
The rule is simple: wherever the time is in the checker, TNB is faster. The question for any workload is how much of its time that phase is — and how much of it pays the JS↔Go boundary instead.
vue-tsc -b (checker-dominated — the big win): the whole-program semantic
pass drops from ~5.5s (JS checker) to ~1.5s (Go checker), and most of stock's
~2.6s full-program parse+bind never happens — TNB's thin program materializes
files lazily, on demand. The rest is Volar codegen and JS-side work both sides
pay. The Go checker's in-process program state is also why TNB's whole-process
RSS runs higher than stock's on this workload — JS heap stays lower; RSS is the
honest whole-process figure.
Editor / LS path (Volar + tsserver): the V8-arena transport (fixed-layout records written straight into V8 memory, DataView reads, interned strings) keeps per-keystroke work near the transport floor. Measured on a 5,537-request roam over the volar corpus (one long-lived session): ~1.0 bridge RPC per request; p50 quickinfo 0.16ms, completionInfo 0.23ms, references 1.5ms (p95 7.4 / 9.0 / 48ms — means are tail-dominated). Per-request byte figures in older revisions of this section described the bridge-internal JS↔Go channel, not the editor-facing tsserver wire.
The carve-out — single-run type-aware ESLint is the workload with the least to gain. Its time is in parsing, AST conversion and rule execution (work both sides pay), and its type-aware queries arrive as tens of thousands of tiny calls (~44K checker RPCs per 1,000 files after the bridge's per-generation memoizing) that measure the JS↔Go boundary, not the engine. TNB lands within ~2% of stock at both sizes — parity, not a win; peak JS heap stays at or below stock. The memory wins live on the long-session editor path (see release notes).
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.