tscからtsgoへ ── DenoのTypeScript基盤はどう変わったか

-- Views

May 22, 26

スライド概要

https://2026.tskaigi.org/talks/2

profile-image

Working at Deno Land, Studying at Georgia Tech

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

TSKaigi 2026 · 2026-05-22 Denoの TypeScript基盤は どう変わったか tsc から tsgo へ ―――― Yusuke Tanaka a.k.a. maguro

2.
[beta]
まず、今日見る現象

スライドURL

// main.ts
import { basename } from "@std/path/basename"; // import map → JSR
import chalk from "npm:chalk@5";

// npm specifier

const bytes: string = await Deno.readFile("./data.bin"); // TS2322
await Deno.connect({ hostname: "example.com", port: 80 });

// deno.json
{ "imports": { "@std/": "jsr:@std/" } }

$ deno check main.ts
TS2322: Type 'Uint8Array' is not assignable to type 'string'.

今日見るのはコードを実行する deno run ではなく、deno check(型チェック)側

3.

LSP も VS Code の赤線・hover も、今日話す 型チェック / 診断 / LSP 側の話 スライドURL

4.

このトークでの用語 スライドURL 用語 このトークでの意味 V8 Isolate ops V8 の独立した実行環境。Denoが実行を制御 V8 isolate 内の JS から Deno Rust 側へ戻る callback TypeScript checker / language service 本体 tsc を Go で再実装したもの Deno専用 fork ではなく、npm から typescript package として配布されるもの TypeScript で書かれたプログラムを実行する deno run したときに通るパス。 deno check したときに通るパス。 TypeScript の型チェックを行う import x from "y" の "y" 部分 tsc / typescript-go stock TypeScript (from npm) 実行パス 診断パス (import) specifier tsgo

5.
[beta]
なぜ実行パスと診断パスの分離が重要?
Deno のソースには、stock TypeScript だけでは決められない情報が混ざる。

import { basename } from "@std/path/basename"; // import map -> jsr:@std/...
import chalk from "npm:chalk@5";

// npm specifier

// Deno namespace
await Deno.readFile("data.bin");
await Deno.connect({ hostname: "example.com", port: 80 });

診断パスでは、これらを TypeScript が扱える形へ渡す必要がある
• specifier / import maps → Deno の resolver / module graph で解決
• Deno.readFile などの Deno API → Deno の lib 型定義として注入
• diagnostics → Deno 側の元ソース位置へ戻して表示
一方、実行パスは、型注釈をswcで剥がし、specifierを解決してからJSとしてV8に渡す
tsc , tsgo は介在しない

スライドURL

6.

Timeline — 実行から診断へ、境界は外へ 古い Deno 2020 〜 〜 2022-04 06 2025-10 2026 が型チェックも担っていた `deno run --no-check` で型チェックを skip 可能に (#6456, #6895) `deno check` 追加、`deno run` 等は no-check default へ (#14072, #14691) forked `tsgo` 実験 → Go版 fork infra 削除 → stock 方向へ `deno run` 最初からきれいに分かれていたわけではない。 実行時の変換と、型チェック / 診断 / LSP の境界がだんだん分離され、外へ動いた。 スライドURL

7.

今日の見取り図 スライドURL このトークで見るのは、Deno が TypeScript checker / language service を どこに置き、Deno固有の module world とどう接続してきたか。 Phase 1 Phase 2 Phase 3 の中 Deno binary の外 Deno project の外 Deno binary embedded `tsc` (V8 isolate) forked `typescript-go` subprocess stock TypeScript from npm

8.

スライドURL Phase 1 embedded tsc in V8 isolate

9.

embedded tsc ベースのアーキテクチャ ┌──────────────┐ request: graph/options ┌─────────────────────────┐ │ Deno Rust │ ══════════════════════════▶ │ │ CLI / LSP │ ◀════ ops: fs/resolve/libs ═ │ JsRuntime / V8 isolate │ 99_main + 97_ts_host │ └──────┬───────┘ │ └───────────┬─────────────┘ │ loads ▼ ▼ ┌──────────────────────┐ │ module graph / cache │ ┌─────────────────────────┐ │ patched `tsc` │ │ npm / JSR / imports │ (00_typescript.js) │ └──────────────────────┘ │ └─────────────────────────┘ を V8 isolate の中で動かし、 ファイル読み込み・モジュール解決などを ops 経由で Deno Rust 側に戻す。 • Rust 側: module graph, resolver, cache, etc. を握る • JS 側: TypeScript の Program オブジェクトを組み立てて、diagnostics / LSP 応答を返す tsc スライドURL

10.

DenoがTypeScriptに見せている世界 側の概念 に見せる形 Deno TypeScript import jsr: package npm: package import maps Deno globals cache-backed source / virtual file name JSR resolver が選んだ package file / 型情報 mode に応じて global cache / ローカル node_modules / BYONM など specifier 解決結果、または tsconfig.json の paths 相当の対応表 Deno.readFile などの型定義を含む *.d.ts https://... にパッチを当てたり、アダプタ層を設けてDeno Rust側と必要な情報をやり取りできるようにすることで、 TypeScript側がDeno独自の世界を正しく解釈できるようにしている tsc スライドURL

11.

Phase 1 を振り返ると Denoの内部に、tsc を抱え込んでいた 状態。 うまく回った点 • tsc 本体に近い挙動を保ちやすい • module graph / resolver / libs を Deno 側で強く制御できる コスト要因 • patched tsc ( 00_typescript.js ) は約 8.7 MiB(release build では zstd 圧縮して同梱) • 上流 tsc への追随コスト — release ごとに fork に patch を cherry-pick → build → rsync する半手動運用 • tsc 用 runtime / isolate を作り、request を実行する設計上のコスト • ただし、実行時のコストは CLI snapshot + V8 code cache により最適化されている • さらに LSP の文脈では open files / unsaved edits / hover / rename / cancellation / diagnostics lifecycle と いった複雑性が加わる スライドURL

12.

スライドURL Phase 2 forked tsgo subprocess

13.

Deno forked tsgo experiment denoland/deno#30920 · MERGED · 2025-10-20 feat(unstable): typescript-go integration for deno check • --unstable-tsgo フラグで有効化される experimental feature • 対象は deno check(型チェック側だけ) • 使ったのは公式の typescript-go ではなく、denoland/typescript-go fork • 理由: Deno固有の概念( http(s): , jsr: , npm: , import maps, etc.)を tsgo に渡す経路が無かった fork は始めるのに便利。 ただ、上流の TypeScript 本体との 差分 (fork drift) を背負うことになる スライドURL

14.

補足: tsgo によるパフォーマンス改善度合いは? スライドURL 計測: Deno 2.7.11 / 7 runs median project denoland/std dsherret/dax scope deno check (44 entrypoints) deno check mod.ts あくまで簡易 bench。Microsoft が公表する一般 benchmark の倍率とは別物 embedded tsc --unstable-tsgo speed-up 3.39 s 1.09 s 1.30 s 0.43 s 2.60x 2.56x

15.

subprocess + stdio IPC スライドURL ┌──────────────┐ ┌──────────────────────┐ │ │ typescript-go --api Deno Rust │ ═══ request: graph/options ═══▶ │ CLI / LSP │ ◀══ callbacks: resolve/... ════ └──────────────┘ │ │ (child process) │ └──────────────────────┘ wire: MessagePack over stdio (protocol adapted from `libsyncrpc`) tsgo は Deno固有の specifier や module graph をそのままでは解決できない。 そこで型チェック中、tsgo が Deno 側に聞きに来る callback を足した。 • モジュールパスの解決 (jsr/npm/URLを実ファイルへ) • 型定義ファイルの参照解決 • module の format 判定 (CJS / ESM) 境界の形は変わった ―― Rust ↔ V8 ↔ tsc から、Rust ↔ child process ↔ tsgo へ。 ただし、Denoの世界を翻訳する仕事 は消えていない

16.

LSP はどうするか? スライドURL の次は LSP (エディタ補完)。ここでも tsgo へ移す試みが続いた。 ただ、両者は扱う形が大きく違う。 deno check 形 主な仕事 状態 キャンセル エディタ) deno check LSP ( 1回起動して終わる ファイルを読んで型エラーを返す なし 不要 エディタが開いている間ずっと動く 補完・hover・rename を、編集に追随して返し続ける プロジェクト全体を持続的に保持 必須 (タイプを追えないと使い物にならない) は「API を1回呼ぶ」で済むが、LSP は エディタとの双方向 protocol を tsgo に被せる必要がある 加えて、テスト・運用インフラそのもののコスト: • 旧 tsc server と tsgo server の両方を切り替えて走らせる test harness が要る • 切り戻し可能な状態を保つ branch 運用も、地味に重い deno check

17.

スライドURL Phase 3 fork を捨てて、公式 npm パッケージへ

18.

転換点 — PR #33133 denoland/deno#33133 · MERGED · 2026-04-02 · net スライドURL 約 5,000 行削減 remove forked typescript-go infrastructure 状態 消えた 残っている 残っている(埋め込み) ( tsgo fork) と、それを支えるインフラ denoland/TypeScript ( tsc fork)— 2026-05-05 に 6.0.3 へ更新 (#32944) cli/tsc/00_typescript.js 約 8.7 MiB の source bundle は今も残る denoland/typescript-go

19.

公式 TypeScript (npm) へ寄せる方向 stock TypeScript = npm install typescript で得られる、Microsoft 公式のパッケージ。 fork を持たず、これを Deno が直接使えるようにする方向。 ここに辿り着くまでに、いくつかの試みがあった: PR #33146 #33160 #33163 status CLOSED MERGED → REVERTED (#33162) OPEN stock tsgo --lsp proxy案。Deno 固有事情の解決が壁に deno tsconfig という独立 subcommand 案 deno install で tsconfig を生成 ← 現在のlive direction スライドURL

20.

PR #33163: stock TypeScript への翻訳 stock TypeScript 向けに、Deno の依存と型をローカル生成する案 つまり、tsc や tsgo にパッチをせず、すでにある仕組みを活用した翻訳を試みる JSR packages Remote modules jsr: を npm.jsr.io から取り、 node_modules/@jsr/... に展開 http(s): Deno globals tsconfig bridge を生成し、 typeRoots + types: ["deno"] で明示読み込み に paths を生成し、 root tsconfig.json から extends node_modules/@types/deno/index.d.ts を CliFileFetcher で取得し、 .deno/remote/... に mirror .deno/tsconfig.json 設計の肝: stock TypeScript が読めるローカル構造へ materialize する。 jsr: / npm: / https: と Deno globals を、 moduleResolution: "bundler" は、: 入り specifier を受けるための妥協点 スライドURL

21.
[beta]
その他、ユーザーから見える変化
診断パス側の変化がすでに Deno main に取り込まれている
// before
const id: number = setTimeout(() => {}, 100);
clearTimeout(id);
// Deno main (#33823, lib.node default)
const id: NodeJS.Timeout = setTimeout(() => {}, 100);
clearTimeout(id); // NodeJS.Timeout | number | undefined

• lib.node が デフォルトで有効 に (#33823, 2026-05-05)
• WebGPU 型は lib.dom 経由に統一 (Deno独自 lib.deno_webgpu は削除)
TypeScript checker から見える Deno の世界を、公式 TypeScript と同じ形 に揃えた

スライドURL

22.

つまり、Phase1,2,3の全体の流れは Phase1(embedded tsc )は、tsc にパッチを当て、V8 Isolate 内で実行 2. Phase2(fork tsgo )は、 境界が1個外に動いた • V8 isolate 内の tsc → forked tsgo をサブプロセスで立ち上げ、IPC で通信 3. tsgo fork ベースのアーキテクチャは取り下げられた • LSP 対応のコスト • そもそも fork を維持しつづけることのメンテナンスコスト 4. そこで Phase3 stock TypeScript from npm を使う方針 • Deno固有の世界を fork なしで どう stock TypeScript と統合するか • deno install + generated tsconfig 案 (#33163) はまだ open 1. スライドURL

23.

スライドURL 他プロジェクトのアプローチ

24.

Oxlint × tsgolint Oxlint (Rust) で no-floating-promises のような 型を見る lint rule (type-aware lint) を実装したい場合 → oxc-project/tsgolint を子プロセスで起動し、その中で type-aware lint を実行 しかし、tsgo の checker / AST / scanner は internal/ に閉じ込められている(非公開) 直接 tsgo API を呼び出している tsgolint は tsgo の非公開 API を公開化する hack を適用し、 詳細: syumai「tsgolintはいかにしてtypescript-goの非公開APIを呼び出しているのか」 スライドURL

25.
[beta]
Vize ── 爆速 Vue toolchain

@ubugeeei による Rust 製の Vue toolchain (vizejs.dev)
bench: 15,000 SFC を 12-core で 373ms / type check は vue-tsc 比 8.9x
tsgo

の使い方

入力 .vue 群

が SFC を分解し、<script lang="ts"> + テンプレート相当の TS を合成
仮想 .ts / .tsx / .d.ts (この段階では in-memory で保持)
↓ tsgo に渡す経路に入ったときだけ `materialize()` を呼ぶ
実際のファイルシステムに書き出し + tsconfig.json を生成
↓

Vize

↓

tsgo

を子プロセスで起動(普通の TypeScript project として読ませる)

ubugeeeiさんにXで質問し、回答をいただきました。ありがとうございます!

スライドURL

26.

Biome スライドURL oxlint、Vize は本家 TypeScript checker を借りる方向。 Biome はその真逆 ── 本家を呼ばず、自前の Rust 製 semantic model で完結する範囲だけを扱う parser → semantic model ↓ 常に走る: scope / references) ( 有効な rule に `RuleDomain::Types` 持ちのものが ├─ 無ければ → そのまま rule 評価 └─ あれば → module graph を walk して 各ファイルで type inference を実行 (fast path) (TypeAware mode) (biome_js_type_info crate) • 利点: Rust binary 一つで完結、subprocess / IPC / JIT warmup なし • 妥協: TS compiler 完全互換ではない、深い型機能 (Conditional / Mapped 等) は追わない • TypeAware mode で動く rule の例: useArraySortCompare , useAwaitThenable ほか

28.

まとめ スライドURL Deno は TypeScript checker / LSP を Deno binary の中 → Deno binary の外 → Deno project の外 へと押し出してきた TypeScript checker Oxlint Vize Biome Deno Phase 3 との橋渡し 子プロセス + tsgo 非公開API hack 仮想 TS を node_modules に materialize 本家を呼ばず、自前 inference で完結 jsr/npm/https/types をローカル生成 + tsconfig で橋渡し tsgolint Vize と Deno Phase 3 には似た設計圧がある ──「fork も再実装も避けたい」制約のもとで、stock checker が読める 形へ materialize する方向へ寄っていく

29.

Who am I スライドURL • Deno Land Inc.で Deno Deploy、Deno Sandbox, Claw Patrol をやって います 🦕 • 最近の興味はDeterministic Simulation Testing, Zig, Networking • 米ジョージア工科大学コンピュータサイエンス専攻修了(2025年12月) GitHub: magurotuna Yusuke Tanaka a.k.a. maguro 𝕏: @yusuktan LinkedIn: yusuktan