# ts-rust (aka tsc-rs)
I wanted to see if LLMs could port the TypeScript compiler, checker and lsp to Rust. Turns out they can.
It [cost over $420,000](#how-did-this-go) in tokens to do it, but you could probably have done it for ~$20k (see below).
## Benchmark: T3 Code with Effect diagnostics
Full type check of [T3 Code](https://github.com/pingdotgg/t3code) with Effect diagnostics on.\*
Time is the sum of the medians for its five checked projects. Lower is faster. Bar length is
in proportion to time. See [Benchmark: T3 Code](#benchmark-t3-code) for the method and the run
without Effect diagnostics.
| Checker | Time | Relative time | Speed vs tsc 7 |
| --- | ---: | --- | ---: |
| `tsc-rs --fast` | 9.77s | █ | 4.35× |
| `tsc-rs` | 14.30s | ██ | 2.98× |
| `tsc` 7 | 42.55s | █████ | baseline |
| `bun check` | 59.24s | ███████ | 0.72× |
| `tsc` 6 | 191.00s | ████████████████████████ | 0.22× |
\* `tsc-rs` is 0.6.0 with Effect diagnostics built in. `tsc` 7 is from `@effect/tsgo`. The
`bun check` time adds a separate `effect-tsgo diagnostics` run. `tsc` 6 uses the JavaScript
Effect plugin.
## Motivations
- Test model capabilities
- Make a fast TypeScript type checker
- Make a ts checker that can work in WASM with high performance
- Memes
## Warnings
**This is an early release.** It has 100% compatibility in every real world project we have
tested. It should work as a drop in
replacement for the vast majority of apps. See [Known problems](#known-problems).
Also worth mentioning: I've never read a line of this code.
## Install
Be warned, I have no idea if this will actually work.
```sh
npm install -D tsc-rs
npx tsc-rs -p tsconfig.json
```
## How did this go?
I used a lot of OpenAI models to try and complete this port. In total I did **over $400,000 in API priced tokens with GPT-5.6 Sol and GPT 6 Astra**. They wrote over 1.3m lines of Rust over multiple months of /goal loops and never got past like 84% compat.
When I saw how little my Claude Code limits were burning, I figured it'd be fun to throw Opus 5.5 at this. It had a working v0 in 10 hours.
I assumed it kept using the code the Codex models wrote. I was wrong. **Opus 5.5 started from scratch. It got further than Astra in 1/10th the time.**
I let it keep going, and it definitely did. Total token spend was **~$24,047 of API spend over 2 weeks**. I was using my Claude accounts, and it worked out to somewhere between **925% and 983% of my $200 plan weekly limits**.
Expensive, for sure, but not that bad considering how much work has gone into typescript-go.
# "The Slop Line"
Everything below this was written by my LLMs, not me.
## What actually is this?
ts-rust is a direct port of Microsoft's native TypeScript compiler, which is written in Go
([microsoft/TypeScript](https://github.com/microsoft/TypeScript), formerly
[typescript-go](https://github.com/microsoft/typescript-go)). It keeps Go's algorithms and
behavior and has the same command line (`tsc`), language server and API.
## Install
```sh
npm install -D tsc-rs
npx tsc-rs -p tsconfig.json
```
`tsc-rs` takes the same options as `tsc`. The npm package is `tsc-rs` so that it does not clash
with the `typescript` package. Each [release](https://github.com/pingdotgg/ts-rust/releases) also
has a standalone archive per platform: the `tsc` binary with the lib files next to it.
Platforms: Linux x64 and arm64 (static, any distribution), macOS arm64, and (from the first
release after 0.2.0) macOS x64, Windows x64 and Windows arm64.
To use it in VS Code, see the [npm package README](npm/tsc-rs-readme.md#vs-code).
## Effect diagnostics
`tsc-rs` has the [Effect](https://effect.website) language service diagnostics built in (codes
377xxx), so an Effect project needs no second compiler. They come from the same check as the
TypeScript diagnostics, and the language server shows them too. They run only when the tsconfig has
the plugin, as with `@effect/language-service`:
```json
{ "compilerOptions": { "plugins": [{ "name": "@effect/language-service" }] } }
```
The rules, options and `@effect-diagnostics` comments are a port of
[Effect-TS/tsgo](https://github.com/Effect-TS/tsgo) 0.51.1. The language server also has its editor
features: the Effect hover (with the Layer summary), inlay hints, the auto-import style, quick fixes,
document symbols, completions and refactors. All the editor features of 0.51.1 are ported. The lightbulb,
the quick fix menu and the editor's Refactor menu show the Effect refactors: as in 0.51.1, the server lists
`refactor.rewrite` in its code action kinds.
The API stability rules of 0.51.1 read the `@stability` JSDoc tags of Effect 4.
`unstableApiUsage` (TS377136) and `experimentalApiUsage` (TS377135) report each use of an
`unstable` or `experimental` API, as warnings by default. `allowedUnstableApis` and
`allowedExperimentalApis` in the plugin entry allow a module (`effect/cli`) or one export
(`effect/cli/Command#make`). `apiStabilityLeak` (TS377137, off by default) is for library
maintainers. So a project on `effect@4` gets TS377136 warnings for its unstable imports, as with
effect-tsgo 0.51.1. See [the Effect diagnostics doc](docs/effect-diagnostics.md#api-stability-rules).
## Status
The port is pinned to one upstream revision, microsoft/TypeScript
[`fed0bf24149f`](https://github.com/microsoft/TypeScript/commit/fed0bf24149fb1ed36039212648bafdafc1ea10e)
(2026-10-08, TypeScript 7.1.0-dev; [UPSTREAM.md](UPSTREAM.md)), and compared with Go at that
revision. To compare, use `typescript@7.1.0-dev.20261008.1` (the same commit), not 7.0.x or
`@typescript/native-preview`. A difference that this build also shows is upstream behavior, and it
goes away when the port moves to a newer pin. tsc-rs 0.3.0 and earlier used the 2026-09-29 pin
(`typescript@7.1.0-dev.20260929.1`).
- **Same results.** TanStack Query core and Hono check with diagnostics identical to Go's. All
184,773 ported Go tests pass. The language server and API answers match Go on the oracle test
sets.
- **Faster.** On 60 open-source projects, type checking takes about half of Go's time (geometric
mean). The npm packages from 0.2.0 on are built in CI with PGO but without BOLT (0.1.0 has
neither), so they are slower than that measured build.
- **Real projects.** On 120 open-source repos, the command-line output differs from Go's only in
the problems below and where Go's own output changes from run to run.
Four switches make some projects faster or use less memory. They are off by default.
- **`--fast`** (or `GOPORT_FAST=1`) turns on the last two switches and more checkers. It runs one
checker per core, up to 16 and at most one per 32 source files, but never fewer than Go's 4
(`--checkers N` still sets the count, and `tsc -b` keeps 4). The cores are the ones that the
process can use (CPU affinity and cgroup limits), so N can be different on another machine.
Each file goes to the checker that Go's split gives it, so the output of `--fast` is the same on
every run and equals `tsgo --checkers N` for the chosen N, except where lazy members or the never
order change the work (see their bullets below). Where Go's output depends on N (zod, below),
the output of `--fast` can be different on another machine. `--fast` is an option of the
compiler command line only (as in Go, `-b` must come first). `GOPORT_FAST=1` also turns on fast
mode in the language server (`--lsp`) and the API server (`--api`). On 16 vCPU Blacksmith
runners (tsc-rs 0.6.0, the benchmark below), VS Code checks in 2.64 s instead of 4.81 s,
Sentry in 2.97 s instead of 5.30 s and T3 Code `apps/server` in 2.78 s instead of 3.60 s.
Across the six apps below, `--fast` is 2.90x the speed of `tsc` 7 against 2.10x for the
default mode (geometric mean). The gain is small or zero on small projects. CPU time went up
as much as 2.2x and peak memory as much as 1.9x in our 16-CPU measurements. On all 16
benchmark cases below, the diagnostics were the same as without `--fast`. On zod, TS2344 and
TS5115 errors in a type cycle can appear or go away, as in Go with more checkers.
`GOPORT_FAST_STEAL=1` (with `--fast`) also turns on work stealing: a checker that has checked
its own files takes unchecked files from the checker with the most work left. It can cut wall
time further on large projects and can cost time on small ones. A file can then go to a
different checker in each run. So where Go's output depends on which files share a checker,
the output can change from run to run (on zod it does).
- **`--checkers 8`** (Go's option; the default is 4) runs 8 checkers. On an 8-core host, VS Code
checks in 30% less time and mui in 23% less, but T3 Code server gains nothing. CPU time goes
up 13% to 51% and peak memory 7% to 34%. The output is the same as Go's with `--checkers 8`.
Go's own output can change from 4 to 8 checkers (on zod, a TS5115 moves).
- **`GOPORT_LAZY_MEMBERS=1`** makes a member of an instantiated class or interface only when the
checker needs it, as microsoft/TypeScript#64475 does (open, not in the pin). Peak memory goes
down (zod 15%, T3 Code server 6.5%, Effect 4%). The time stays about the same. The
`--extendedDiagnostics` Symbols count is then lower than Go's. Diagnostics and emit stay the
same as Go's, except when the members of a type resolve inside their own resolution (a cycle):
there, as with #64475 in Go, a TS2589, TS5114 or TS5115 can move to another node or go away.
None of the tested projects has such a case.
- **`GOPORT_FAST_NEVER_ORDER=1`** checks some intersection types in a faster order (mongodb's
test project: 8.9 s to 1.6 s). The `--extendedDiagnostics` counts are then lower than Go's.
Diagnostics and emit stay the same as Go's, except near the TS2589 limit (5,000,000 type
instantiations): there, a TS2589 that Go reports can go away.
## Benchmark: T3 Code
Full type check of [T3 Code](https://github.com/pingdotgg/t3code) at
[`cd41c4ad`](https://github.com/pingdotgg/t3code/tree/cd41c4ada0c70cc2eec95ecd7266f3dab010c58c),
with and without Effect diagnostics. Each time is the **sum of the medians** for `apps/server`,
`apps/web`, `apps/mobile`, `packages/client-runtime` and `packages/shared`, not the elapsed time
of the parallel benchmark workflow. Lower is faster.
**With Effect diagnostics**
Bar length is in proportion to time.
| Checker | Time | Relative time | Speed vs tsc 7 |
| --- | ---: | --- | ---: |
| `tsc-rs --fast` | 9.77s | █ | 4.35× |
| `tsc-rs` | 14.30s | ██ | 2.98× |
| `tsc` 7 | 42.55s | █████ | baseline |
| `bun check` | 59.24s | ███████ | 0.72× |
| `tsc` 6 | 191.00s | ████████████████████████ | 0.22× |
With Effect, `tsc-rs --fast` is fastest: 4.35× the Effect-patched `tsc` 7 and 6.1× Bun
followed by the separate Effect pass. On the smallest projects (`apps/mobile`,
`packages/shared`) `--fast` gains little or nothing.
Without Effect diagnostics
Bar length is in proportion to time.
| Checker | Time | Relative time | Speed vs tsc 7 |
| --- | ---: | --- | ---: |
| `bun check` | 6.24s | ██ | 3.12× |
| `tsc-rs --fast` | 6.91s | ██ | 2.81× |
| `tsc-rs` | 7.95s | ██ | 2.45× |
| `tsc` 7 | 19.45s | █████ | baseline |
| `tsc` 6 | 87.87s | ████████████████████████ | 0.22× |
`tsc-rs` is 0.6.0. Bun is fastest without Effect.
Diagnostics matter when comparing these times:
- Without Effect, `tsc-rs` and `tsc-rs --fast` match the pinned `tsc` 7 diagnostics exactly.
Both Rust modes and `tsc` 6 report the same TS2322 in the server fixture. Bun reports no
TypeScript errors.
- With Effect, `tsc-rs`, `tsc-rs --fast` and the native Effect compiler agree on
**4,823 Effect diagnostics**, including 4,602 unstable-API warnings. The separate Effect
pass after Bun reports the same rules and messages, with different path and severity
formatting.
- The native Effect compilers also report 26 TS2790 diagnostics. `tsc-rs` additionally
reports the TS2322 above, which the pinned `tsc` 7 also reports; the Effect compiler's older
TypeScript version does not. The JavaScript Effect plugin uses a different rule set and
reports 305 diagnostics in total. These tools do not all perform identical checks.
Measured on 2026-10-13 UTC, on Blacksmith `blacksmith-16vcpu-ubuntu-2404` runners
(16 AMD EPYC vCPUs, about 62 GiB RAM). Each project/mode has its own runner; all five checkers
for that case run on that same runner. Every cell has 5 runs after 1 warmup, with
`--noEmit --incremental false --composite false` and default thread counts. TypeScript 6
runs on Node 24.19.0 with a 16 GiB heap.
Without Effect, the configs have no Effect plugin and `tsc` 7 is 7.1.0-dev.20261008.1 (the
port's pin). With Effect, `@effect/tsgo` 0.51.1 supplies its TypeScript 7.0.2 executable; the
JavaScript comparison uses TypeScript 6.0.3 and `@effect/language-service` 0.87.4. Bun is
1.4.4-canary.1+c69a636b9. `tsc-rs` is 0.6.0: a release PGO build without BOLT of the tag
source ([`a8c5b7063`](https://github.com/pingdotgg/ts-rust/commit/a8c5b7063eed1ba2929ee78a7dd3508ebd43a6f8)), the same build
process as the npm packages. `tsc-rs --fast` is the same binary with `--fast`.
[Results, samples, hashes and per-case metadata](docs/benchmarks/2026-10-13-blacksmith.json)
come from [this Blacksmith run](https://github.com/pingdotgg/ts-rust/actions/runs/38461793814),
which also measured the app benchmark below.
The [workflow](.github/workflows/bench-apps.yml) caches the verified compiler and prepared
inputs before running the comparisons in parallel. The T3 Code runner is
[scripts/bench-apps/t3code.sh](scripts/bench-apps/t3code.sh).
## Benchmark: real-world apps
Full type check of six open-source apps on Blacksmith `blacksmith-16vcpu-ubuntu-2404`
runners (16 AMD EPYC vCPUs, about 62 GiB RAM). Each app has its own runner; all five checkers
for an app ran on that runner. Lower times are faster.
| App | `tsc` 6 | `tsc` 7 | `tsc-rs` 0.6.0 | `tsc-rs` 0.6.0 `--fast` | `bun check` |
| --- | ---: | ---: | ---: | ---: | ---: |
| [VS Code](https://github.com/microsoft/vscode/tree/3f07e1aba32acacb8b08ae91bfdc954b580ad1fd) | 91.75s | 9.57s | 4.81s | 2.64s | 2.32s |
| [Sentry (frontend)](https://github.com/getsentry/sentry/tree/8294650589dbd26f230c73f4ab26b62a68aede8f) | 80.27s | 13.49s | 5.30s | 2.97s | 6.93s |
| [Playwright](https://github.com/microsoft/playwright/tree/d469960fdfc461e2d5795a3fa48a58a52a91ecaf) | 6.33s | 0.82s | 0.41s | 0.33s | 0.27s |
| [Excalidraw](https://github.com/excalidraw/excalidraw/tree/53973c3a423fbd75a4ce68107786b4fcb90e4968) | 6.85s | 1.42s | 0.62s | 0.48s | 0.24s |
| [TypeORM](https://github.com/typeorm/typeorm/tree/c64a1f052fc39f6688b6b73b83d065d7147ba8bb) | 5.60s | 0.70s | 0.39s | 0.27s | 0.15s |
| [tRPC (server package)](https://github.com/trpc/trpc/tree/d756e591a5e37ef20b8d75ecd4d736c195497289) | 1.55s | 0.17s | 0.09s | 0.09s | 0.10s |
| **Geometric mean vs `tsc` 6** | **1.0×** | **7.3×** | **15.3×** | **21.2×** | **24.0×** |
`tsc-rs` 0.6.0 is **2.10× faster than `tsc` 7** across these apps (geometric mean).
The corresponding figures are 2.90× for `tsc-rs --fast` and 3.29× for Bun. Bun is fastest
on four apps; `tsc-rs --fast` is fastest on Sentry and `tsc-rs` on tRPC, where `--fast`
gains nothing (0.088 s against 0.085 s).
Diagnostics differ between compilers. `tsc-rs`, `tsc-rs --fast` and the pinned `tsc` 7
produce identical full diagnostics: 10 on VS Code, 2 on Sentry, and none on the other four
apps. `tsc` 6 reports 9 on VS Code and none on the others. Bun reports none on any app.
The time comparisons do not imply identical checking behavior for Bun or `tsc` 6.
Measured on 2026-10-13: median of 5 runs after 1 warmup, `--noEmit --incremental false`,
default thread counts, and direct native binaries. `tsc` 6 runs on Node 24.19.0 with a 16 GiB
heap. Versions: TypeScript 6.0.3 and 7.1.0-dev.20261008.1 (the port's pin), `tsc-rs` 0.6.0,
and Bun 1.4.4-canary.1+c69a636b9. The `tsc-rs` build is release PGO without BOLT, static
musl, built from the tag source with the same training process as the release workflow.
[Results, samples, hashes and input revisions](docs/benchmarks/2026-10-13-blacksmith.json)
come from [this Blacksmith run](https://github.com/pingdotgg/ts-rust/actions/runs/38461793814),
the same run as the T3 Code benchmark above. Every job passed.
The pinned app inputs use the same preparation as the earlier benchmark:
- Excalidraw: remove `baseUrl`, which native TypeScript no longer supports.
- TypeORM: use `nodenext` module resolution.
- VS Code: install the Electron typings normally supplied by its postinstall.
- Playwright: generate the injected sources normally supplied by its build.
The scripts are in [scripts/bench-apps](scripts/bench-apps). The
[benchmark workflow](.github/workflows/bench-apps.yml) caches the verified compiler and each
prepared project, then runs each comparison on a separate runner of the same size. Each
comparison keeps all compilers on one runner and saves their full output and exit codes.
### Why `bun check` is faster
The lead of `bun check` is all in the check step. Its checkers share one type store on all cores.
`tsc` 7 runs 4 checkers, and each one makes its own copies of the types it needs: on T3 Code
`apps/server`, 57% of the types. `tsc-rs` keeps the `tsc` 7 model, because its goal is output that
is identical to `tsc` 7. More checkers or one shared store would change that output, so `tsc-rs`
does not copy them. `--checkers 8` uses more cores with output identical to `tsc` 7 at
`--checkers 8` (see [Status](#status)).
Per thread, `tsc-rs` is not slower. On T3 Code it was 1.5× to 7× faster per thread than
`bun check`, and on 28 other open-source repos the two did about the same check work per thread.
`tsc-rs` also loads and parses files as fast as `bun check` or faster.
The output is not the same. `bun check` gave output identical to `tsc` 7 on 37 of 60 open-source
repos, and `tsc-rs` on all 60. `bun check` also reports errors that `tsc` 7 does not, for example
TS2749 on T3 Code `apps/server`. These numbers are from our studies of 2026-10-07 and 2026-10-08,
with `tsc-rs` 0.1.0 and R179 on Linux.
## Known problems
- In some monorepos, the source files of a workspace package are reachable both through
`node_modules` and through a direct import. There, `tsc-rs` can write output for more of those
files than `tsc` does, and report TS6059 (file is not under `rootDir`) for them. `tsc` decides
this by timing, so its own result changes between runs. `tsc-rs` gives the same result in every
run (the result of TypeScript 6).
- In `tsc -b`, when one project imports the output of another project without a project reference,
the two projects can build at the same time. Then `tsc` reads each output file before or after the
other project writes it. This is timing, so the result of `tsc` changes between runs, file by
file. `tsc-rs` makes the programs one at a time and writes the outputs of a project together when
that project ends. So it can read old or missing output where `tsc` reads new output, or the
reverse. Often the only difference is the file hashes in the reading project's `.tsbuildinfo`.
Sometimes it is an error, such as TS2305, TS2307 or TS2322. Add the reference to fix it.
- In the editor, memory grows slowly during long edit sessions: 3.1 to 4.6 MiB per 1,000 edits in
our 10,000-edit sessions, where `tsc` grows 2.6 to 2.8. It starts within 10% of `tsc`'s, and from
about edit 20 it stays below `tsc`'s. After 10,000 edits (two sessions each) it was 232 and 249
MiB against `tsc`'s 291 and 305 MiB on Query core, and 314 and 324 MiB against 417 and 422 MiB on
Hono.
- `tsc-rs --version` prints the TypeScript version that it ports (7.1.0-dev), not the npm
version. The compiler matches `typesVersions` against it.
## Development
`crates/ts_goport` is the compiler. It has three parts crates in `crates/ts_goport/parts`:
`goport_core` (everything below the language service), `goport_util` and `goport_lsproto`. Their
files stay in `crates/ts_goport/src`. It uses the lib files in `crates/ts_goport/libs`.
`tools/ts_ast_codegen` generates `crates/ts_goport/src/astdata`, and
`tools/ts_diagnostics_codegen` generates `crates/ts_goport/src/diagnostics/catalog.rs` and
`crates/ts_goport/src/diag.rs`. `crates/ts_wasm` is the WebAssembly build
([npm/wasm](npm/wasm/README.md)).
```sh
./scripts/run-cargo-capped.sh build --release -p ts_goport --bins
./scripts/verify.sh
```
The bins are `goport` (type check) and `tsgo` (the Go `tsgo` command line). The Go baseline tests
run with
`TS_GO_REPO=/path/to/typescript-go ./scripts/run-cargo-capped.sh test -p ts_goport --test go_baselines`.
- Port rules: [crates/ts_goport/PORTING.md](crates/ts_goport/PORTING.md)
- Measurement and gate scripts: [scripts/goport](scripts/goport/README.md)
- npm packages and releases: [npm/README.md](npm/README.md)
- Typechecker work rules: [AGENTS.md](AGENTS.md), the
[accountability rules](docs/typechecker-accountability.md) and the
[saved state](docs/typechecker-state/current.json)
- How the project started: [docs/history.md](docs/history.md)
### Develop on Windows
CI builds and tests `x86_64-pc-windows-msvc` (the Windows job of
[ci.yml](.github/workflows/ci.yml)).
- Install Rust with the MSVC toolchain (rustup's default on Windows) and the Visual Studio Build
Tools (C++ workload).
- Clone with LF line endings: `git clone -c core.autocrlf=false ...`. `.gitattributes` forces LF
for `*.rs`, `*.sh` and the bundled libs (the lib snapshot tests hash their bytes), but the test
fixtures need LF too.
- Use `cargo` directly. `scripts/run-cargo-capped.sh` needs `systemd-run`.
- Run the tests with `RUST_TEST_THREADS=1`, as CI and `goport-tests.sh` do. The Go baseline tests
need `TS_GO_REPO` and Node, as on Linux (see the CI job for the setup).
- Two `fswatch_watcher` tests (`test_subscribe_symlink_create` and `_delete`) make file symlinks,
which need Developer Mode (Settings, System, For developers) or an elevated shell. Without
either they fail with OS error 1314.
- The scripts are bash. The release scripts (`crates/ts_goport/scripts/build-pgo.sh`,
`copy-libs.sh`, `scripts/goport/pgo-train.sh`) and `scripts/goport/win-go-compare.sh` run in
Git Bash. The other tools in `scripts/` (the revision pipeline, the gate, `remote.sh`) are for
the Linux hosts only.
## Releases
Push a tag `v` (for example `v0.1.0`). The
[release workflow](.github/workflows/release.yml) builds, packs and tests the packages, publishes
them to npm and creates a GitHub release. A stable version goes to the dist-tag `latest`, and a
prerelease version (`v0.2.0-beta.1`) to `next` and a GitHub prerelease. See
[npm/README.md](npm/README.md#tsc-rs-releases).
## License
[MIT](LICENSE). The port keeps the licenses and notices of the code it ports: TypeScript
(Apache-2.0) and parts of the Go standard library (BSD-3-Clause). See [NOTICE.md](NOTICE.md).