同じ TypeScript ソース版を使い、tsgo と tsrs で 4 つの合成ケースを実行。診断と終了コードはすべて一致。
図:4 つの単一ファイル合成ケースで tsgo と tsrs の診断を比較。すべて一致していますが、実プロジェクトの互換性や性能を示すベンチマークではありません。

Cloudflareがtsrsを紹介したとき、「TypeScript 7.0に比べて最大8倍高速、メモリ使用量は約70%減」という数字が出ていました。ところがtsrsの特定コミットに固定された古いベンチマークを開くと、mui-docsは16.87倍、vscodeのデフォルト設定でも13.55倍。最初に気になったのは、公式発表の8倍とどう整合するのか、という点です。

その大規模プロジェクトの速度測定を私が再実行したわけではありません。今回は別の小さな検証として、tsrsとtsgo(TypeScriptのGo実装)を対応するバージョンに固定し、GitHubがホストする一時的な隔離環境で4つのTypeScriptサンプルを実行しました。4ケースの診断は一致。ただし、それだけで本番CIの型チェックを置き換えるつもりはありません。

ベンチマークを読むとき、なぜchecker数を見るのか

Cloudflareの発表が示したのは初期比較の「最大8倍、メモリ約70%減」という要約です。一方、tsrsの固定コミットにある履歴ベンチマークには、ケースごとの数値が載っています。

過去の上流ベンチマークtsgotsrschecker数報告上の倍率
mui-docs, デフォルト12.66 s0.75 s4 vs 1616.87倍
mui-docs, 両方 --checkers 412.69 s1.07 s4 vs 411.81倍
mui-docs, 両方 --checkers 1612.36 s0.77 s16 vs 1616.07倍
vscode, デフォルト11.10 s0.82 s4 vs 1613.55倍
vscode, 両方 --checkers 166.73 s0.81 s16 vs 168.32倍

デフォルトの2行は、そもそもcheckerの数が違います。tsgoは4、tsrsは16です。それでも双方を4または16にそろえた行でも8倍を超えています。ここで混同したくないのは、発表文の数字が初期測定の概括で、RESULTS.md は別の固定バージョンと実行環境に基づく履歴スナップショットだということです。「8倍が表の上限」でも「自分のプロジェクトでも16倍になる」でもありません。

なぜここまで差が出るのかを、表だけから一つの理由に断定することはできません。記録されている条件は、Depot CIのdepot-ubuntu-24.04-16(16 vCPU、63 GB RAM、Linux x86_64)で交互に測定した実行時間とピークメモリの中央値。tsgo側はtypescript@7.0.2、tsrs側はより新しいTypeScript 7.1-devを移植し、PGO最適化したリリースビルドを使っています。checker数が同じでも、ソースの版とビルド条件までは一致しません。

メモリ使用量も条件で変わります。mui-docsはデフォルト設定で4.61倍少ないと記載され、双方を16 checkerにすると8.23倍少ないという数値になります。「約70%減」は初期結果の要約であって、どのプロジェクトにも当てはまる保証ではありません。この節の数値は上流レポートから確認したもので、私が大規模ベンチマークを再測定した結果ではありません。

4つの診断ケースで実際に確かめたこと

tsrs@0.18.0-ts7.1.0-dev.20260929 と、Go実装を配布する typescript@7.1.0-dev.20260930.4 を固定しました。両者は同じTypeScriptソースコミット b85298b6 に対応しています。

依存関係の準備では一度つまずきました。TypeScriptソースリポジトリのworkspace名 @typescript/typescript をnpmで公開されているパッケージだと思って指定したところ、E404が返り、コンパイラは起動すらしていません。公開されているルートパッケージは typescript。このnightlyではCLI名は引き続き tsc で、Linux x64のネイティブGo実装は @typescript/typescript-linux-x64 から提供されます。パッケージ名を直して初めてコンパイルまで進みました。

4つのケースは同じ tsconfig.json を使います。types: [] はテスト環境のグローバルな @types を自動的に読み込ませないため、skipLibCheck: false は宣言ファイルの型検査を省略しないためです。単に「strictモード」と書くより、実際の設定を示した方が再確認しやすくなります。

{
  "compilerOptions": {
    "strict": true,
    "target": "es2022",
    "module": "esnext",
    "noEmit": true,
    "skipLibCheck": false,
    "types": []
  },
  "include": ["src/*.ts"]
}

各ケースのディレクトリには src/test.ts が1つだけあります。内容は次のとおりです。

ケースsrc/test.ts の全内容実測結果(tsgo/tsrs)
validexport const answer: number = 42;診断0件・終了0/0
type-errorexport const answer: number = "wrong";TS2322、終了2/2
module-missingimport {missing} from "./unavailable.js"; export const answer = missing;TS2307、終了2/2
generic-constrainttype OnlyText<T extends string> = {value:T}; export type Bad = OnlyText<number>;TS2344、終了2/2

双方に --noEmit --pretty false --checkers 4 を渡しました。ファイル名、行・列位置、エラーコード、診断メッセージ、順番、プロセスの終了コードを比較し、4ケースすべて一致しました。ただし1ケース1ファイルなので、--checkers 4 を指定しても4並列の性能を検証したことにはなりません。この設定は比較条件を明示しただけです。

エラー件数が同じでも安心できない理由

エラーの合計件数が合っているだけでは不十分です。tsrsの固定バージョンの互換性資料では、13,462件のエラーベースライン中13,458件が一致し、.types と .symbols の12,779件はすべて一致したと報告されています。

drizzle-orm は分かりやすい例です。上流レポートでは双方が16 checkerで10,846件のエラーを検出しましたが、診断の出る位置は違い、tsrsのバグとして明記されています。これは私が再現した例ではなく、固定された報告書で確認した内容です。CIやコードレビューでは件数が合っていても、間違ったファイルや行を指すなら困ります。

ほかにもバージョン固有の制限があります。--init と --showConfig は未対応、--locale は診断を翻訳しません。.astro、.vue、.svelte のcontent mapperはCLI/buildには対応しますが、言語サーバーには未対応。配布済みバイナリもWindowsとIntel Macをカバーしていません。今回試したのはGitHub上の隔離Linux環境でのCLIだけで、それらのOSやIDEプラグイン、LSPは触れていません。astro check やエディタのリアルタイム診断に依存するなら、まず該当バージョンの互換性表を確認してください。Astroアップグレードの型チェックで困っている場合は、以前書いた互換性の調査メモもあります。

自分のプロジェクトで試すなら何を比較するか

いきなり普段の作業ディレクトリを変えず、使い捨てのworktreeか別ブランチで試します。うまくいかなくても、そのまま破棄できるためです。

次のスクリプトは、「終了コードは同じでも診断テキストが違う」という見落としを防ぎます。set +e は意図的です。エラーを含むケースは正常に非ゼロで終了するので、両方を最後まで実行し、終了コードを保存してからdiffを取る必要があります。ここでの tsc は固定したTypeScript nightlyのGo実装で、/tmp/tsgo.out はログファイル名にすぎません。

pnpm add -D --save-exact tsrs@0.18.0-ts7.1.0-dev.20260929 typescript@7.1.0-dev.20260930.4

set +e
pnpm exec tsc -p . --noEmit --pretty false --checkers 4 > /tmp/tsgo.out 2>&1
tsgo_status=$?
pnpm exec tsrs -p . --noEmit --pretty false --checkers 4 > /tmp/tsrs.out 2>&1
tsrs_status=$?
diff -u /tmp/tsgo.out /tmp/tsrs.out
diff_status=$?
set -e
printf 'tsgo_exit=%s tsrs_exit=%s diff_exit=%s\n' "$tsgo_status" "$tsrs_status" "$diff_status"
if [ "$tsgo_status" -ne "$tsrs_status" ] || [ "$diff_status" -ne 0 ]; then
  echo 'DIAGNOSTIC_DIFF_OR_EXIT_MISMATCH: inspect /tmp/tsgo.out and /tmp/tsrs.out'
  exit 1
fi

最初に差が出たら、生の出力を保存します。そのうえで --checkerAssignment go(tsgoに近いファイル割り当てとchecker履歴)か --noLazyMembers(tsrsのlazy-member最適化を無効化)を個別に試します。最初から有効にすると、デフォルト動作の差を見失います。詳しくは使い方とデフォルト値の相違を参照してください。

上のスクリプトは診断の比較だけで、時間やメモリを測りません。今回、私は性能測定全体までは実施していません。手元のプロジェクトで一度記録するなら、LinuxではGNU time で実行時間とピークRSSを取得できます。macOSでは gnu-time を入れ、最初の行を TIME_BIN=gtime に変更します。

TIME_BIN=/usr/bin/time

set +e
"$TIME_BIN" -f 'elapsed_s=%e max_rss=%M' -o /tmp/tsgo-bench.metrics \
  pnpm exec tsc -p . --noEmit --pretty false --checkers 4 \
  > /tmp/tsgo-bench.out 2>&1
tsgo_status=$?
"$TIME_BIN" -f 'elapsed_s=%e max_rss=%M' -o /tmp/tsrs-bench.metrics \
  pnpm exec tsrs -p . --noEmit --pretty false --checkers 4 \
  > /tmp/tsrs-bench.out 2>&1
tsrs_status=$?
set -e

printf 'tsgo_exit=%s ' "$tsgo_status"; cat /tmp/tsgo-bench.metrics
printf 'tsrs_exit=%s ' "$tsrs_status"; cat /tmp/tsrs-bench.metrics

ただし、この2コマンドから得られるのは一組だけの数字です。裏で何か走っていれば簡単に変動します。本当に比較するなら同じrunnerで実行順を入れ替えながら少なくとも5組測り、依存lockfile、コンパイラの版、tsconfig、checker数を固定して中央値とピークRSSを見ます。コールドスタートとウォームキャッシュの測定も分けます。私はOSのpage cacheをクリアしていないため、完全なシステムコールドキャッシュの測定とは呼びません。GNU time の %M は通常KiB単位のピークRSSなので、異なる環境を比較する前に単位を確認します。

私ならまずtsrsをCIの追加・非ブロックジョブとして並走させ、従来の tsc とフレームワーク側の検査は必須ゲートとして残します。診断差分が説明でき、速度とメモリの測定が安定し、実際のPR/リリースを一巡してロールバック手順も確認できてから、置き換えを検討します。小さな4ケースで分かったのはあくまで、この入力に対する診断が両者で一致するという一点です。

実測日:2026年10月22日