Upgrade to Pro — share decks privately, control downloads, hide ads and more …

型の深宇宙へ飛び込め — TSKaigi 2026 LT

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for KintoTech_Dev KintoTech_Dev
May 22, 2026
14

型の深宇宙へ飛び込め — TSKaigi 2026 LT

Avatar for KintoTech_Dev

KintoTech_Dev

May 22, 2026

More Decks by KintoTech_Dev

Transcript

  1. TSKaigi 2026 — LT 型の深宇宙へ飛び込め O P E N あなたのプロジェクトの

    tsc --noEmit 何秒? それ、型のせいかもしれません。 @dOwOd 01 / 28
  2. TSKaigi 2026 — LT 2026-05-22 1 0 M I N

    型の深宇宙へ 飛び込め tsc を遅くする 記述パターンの全解剖 TS 7.0 tsgo は 約 10 倍 高速化される。 でも 遅い型は 遅いまま @dOwOd · KINTO Technologies 02 / 28
  3. 型の深宇宙へ飛び込め § 0 · INTRO 自己紹介 齋藤 諒太 / dowod

    所属 KINTO テクノロジーズ株式会社 仕事 クルマのサブスク KINTO の シミュレーション / マイページ 開発 𝕏 @_dowod @dOwOd @dOwOd · TSKaigi 2026 LT 03 / 28
  4. 型の深宇宙へ飛び込め § 1 · MENTAL MODEL 大前提 型は コンパイル時に走るプログラム types.ts

    type Pick<T, K extends keyof T> = { [P in K]: T[P] }; // 型「関数」の定義 type U = Pick<User, "id">; // ^^^^^^^^^^^^^^^^^ これが関数の「呼び出し」 @dOwOd · TSKaigi 2026 LT 04 / 28 型を呼ぶ = instantiation(インスタンス化) Instantiations の数 = 計算回数 今日の話 = 「計算回数が爆発する 3 パターン」と、その対処法
  5. 型の深宇宙へ飛び込め § 1 · ONE-LINER 今日持って帰ってもらう 3 行 再帰は 溜める

    RECURSION — accumulate Union は 包む UNION — wrap before distribute Mapped は 分けて固める MAPPED — split keys & values @dOwOd · TSKaigi 2026 LT 05 / 28 3 つの「重さの種類」と、その対処法 どれも tsgo で速くなる。けど、アルゴリズムは同じ
  6. 型の深宇宙へ飛び込め § 2.1 · RECURSION 再帰 R E C U

    R S I O N よくある業務例 split.ts // ts-pattern / typed-routes / Notion API path / i18n key … type Split<S extends string, D extends string> = S extends `${infer H}${D}${infer T}` ? [H, ...Split<T, D>] : [S]; type Path = Split<"/users/:id/posts/:postId", "/">; // ^ ["", "users", ":id", "posts", ":postId"] ※ N = 文字列を区切った個数(= 再帰の深さ) 。例: "/users/:id/posts/:postId" を / で分けると N=5 @dOwOd · TSKaigi 2026 LT 06 / 28 § 1 ルーティング・パスパース・i18n キー・型レベル CSV パーサ 動くうちは便利、N が伸びると突然 TS2589 で止まる
  7. 型の深宇宙へ飛び込め § 2.2 · WHY 再帰 W H Y H

    E A V Y なぜ重い? Split<"a/b/c", "/"> Split<"b/c", "/"> Split<"c", "/"> ["c"] ⏳ 答え待ち ⏳ 答え待ち ⏳ 答え待ち ✓ 答え! 前段は答えが来るまで返れない → N 段ネスト = N 個の instantiation がスタックに積まれる @dOwOd · TSKaigi 2026 LT 07 / 28 § 1
  8. 型の深宇宙へ飛び込め § 2.3 · IDEA 再帰 B E F O

    R E / A F T E R 解決策のイメージ Before: 積まれる Split<"a/b/c", "/"> Split<"b/c", "/"> Split<"c", "/"> ["c"] ⚠ 答え待ちが N 段積まれる After: 流れる(溜める) Split<"a/b/c", "/", []> Split<"b/c", "/", ["a"]> Split<"c", "/", ["a","b"]> ["a","b","c"] ✓ 答えを溜めて次に渡す @dOwOd · TSKaigi 2026 LT 08 / 28 § 1 答えを アキュムレータに溜めて 次に渡す → 「答え待ち」が無いので積まれない
  9. 型の深宇宙へ飛び込め § 2.4 · MEASURE 再帰 M E A S

    U R E 計測 サイズ BEFORE (ACC なし) AFTER (ACC あり) N=20 20 ms · 310 inst 20 ms · 292 inst N=60 TS2589 (693 inst で頭打ち) 30 ms · 812 inst N=200 ( 展開不能) 50 ms · 2,632 inst N=900 ( 展開不能) 190 ms · 11,732 inst ※ 時間 = tsc 内部の Check time ( tsc --extendedDiagnostics で実測) 。inst = Instantiations = 型関数の呼び出し回数 @dOwOd · TSKaigi 2026 LT 09 / 28 § 1
  10. 型の深宇宙へ飛び込め § 2.5 · FIX 再帰 F I X 解決策:

    「溜める」 split.tail.ts // Before: 戻り値で再帰 ( 末尾位置でない) type Split<S, D> = S extends `${infer H}${D}${infer T}` ? [H, ...Split<T, D>] : [S]; // After: アキュムレータに溜めて末尾再帰に type Split<S, D, Acc extends string[] = []> = S extends `${infer H}${D}${infer T}` ? Split<T, D, [...Acc, H]> // ← 末尾位置 ! : [...Acc, S]; PR #45711 で TS は末尾再帰を 最大 1000 段 まで畳む @dOwOd · TSKaigi 2026 LT 10 / 28 § 1
  11. 型の深宇宙へ飛び込め § 3.1 · UNION Union U N I O

    N よくある業務例 events.ts type Events = "click" | "hover" | "drag" | "drop" | /* … 数十個 */; type EventMap = { [E in Events]: { type: E; payload: Payload<E> }; }[Events]; // ← Union 展開 @dOwOd · TSKaigi 2026 LT 11 / 28 § 2 業務でよく出る: イベント名 / ステータス / role / API レスポンス種別 Union のサイズ × 中の重さ で爆発
  12. 型の深宇宙へ飛び込め § 3.2 · WHY Union D I S T

    R I B U T I V E なぜ重い? distribute.ts // Handler<E> = 自前のユーティリティ型(E から payload / handler を引く、など) E = "click" | "hover" | "drag" | ... (N 個) E extends string ? Handler<E> : never ↓ TS が要素ごとに展開 (distributive conditional) Handler<"click"> | Handler<"hover"> | Handler<"drag"> | ... ↑ N 個の独立した instantiation CROSS-PRODUCT N × c サイズ N 個分の 独立した計算 @dOwOd · TSKaigi 2026 LT 12 / 28 § 2 Union のサイズ N × Handler 1 回の計算量 c = N × c N=1000, c=150 → 150,000 instantiation 「Union を渡したら、中で勝手にバラバラにされる」という TS の振る舞い
  13. 型の深宇宙へ飛び込め § 3.3 · MEASURE Union B E F O

    R E / A F T E R 計測 サイズ BEFORE AFTER N=20 3,000 inst 40 inst N=500 75,000 inst 1,000 inst N=1000 150,000 inst 2,000 inst N=1000 比 75× の instantiation 差 @dOwOd · TSKaigi 2026 LT 13 / 28 § 2
  14. 型の深宇宙へ飛び込め § 3.4 · FIX Union W R A P

    解決策: 「包む」 wrap.ts // Before: Union を毎要素ごとに分配 type Map = { [E in Events]: Handler<E> }; // After: 一度 record (キー→ 値の表)にまとめてから走査 type EventTable = { click: ClickPayload; hover: HoverPayload; drag: DragPayload }; type Map = { [K in keyof EventTable]: EventTable[K] }; // ^ keyof で 1 個ずつ取り出すから分配されない Union を 個別に当てる前に、一度オブジェクト型(record)にまとめる @dOwOd · TSKaigi 2026 LT 14 / 28 § 2
  15. 型の深宇宙へ飛び込め § 4.1 · MAPPED Mapped M A P P

    E D よくある業務例 paths.ts // zod schema / GraphQL Codegen / form path type FormPaths<T> = { [K in keyof T as `${string & K}.${number}`]: T[K] }; type UserPaths = FormPaths<User>; // ← as 句で全展開 @dOwOd · TSKaigi 2026 LT 15 / 28 § 3 as 句が入ると homomorphic 性が崩れる
  16. 型の深宇宙へ飛び込め § 4.2 · CACHE Mapped C A C H

    E L O S T なぜ重い? homomorphic.ts // 軽い { [K in keyof T]: T[K] } ↳ TS: 「T の形そのまま! 」 → cache OK ✓ (T 1 個の評価で済む) as-remap.ts // 重い { [K in keyof T as Rename<K>]: T[K] } ↳ TS: 「キーが変わる… 元の形不明」 → cache 無効 ✗ ( 全 key を独立に評価) ※ 根拠: TypeScript 3.1 で導入された homomorphic mapped type の概念。 as 句でこの性質が崩れる (Issue #40619) @dOwOd · TSKaigi 2026 LT 16 / 28 § 3 homomorphic = 「T と同じ構造を保つ」性質 as 句で homomorphic が消える → cache 失効 key 数 K × 各 K の評価コスト = K 個の独立 instantiation
  17. 型の深宇宙へ飛び込め § 4.3 · MEASURE Mapped I N S T

    A N T I A T I O N S 計測 K (KEY 数) HOMOMORPHIC ( AS 句なし) AS 句あり K=20 9 inst 96 inst K=200 9 inst 816 inst K=1,000 9 inst 4,016 inst K=10,000 9 inst 40,016 inst → homomorphic は K に依存せず 9 inst で一定 / as 句あり は K に線形比例 (K=10,000で約 4,400× の差) ※ tsc 6.0 + --extendedDiagnostics 実測。 homomorphic = { [K in keyof T]: T[K] } 、as 句あり = { [K in keyof T as Rename<K>]: T[K] } @dOwOd · TSKaigi 2026 LT 17 / 28 § 3
  18. 型の深宇宙へ飛び込め § 4.4 · FIX Mapped S P L I

    T & F R E E Z E 解決策: 「分けて固める」 split-keys.ts // Before: as 句で動的にキーを生成 type Paths<T> = { [K in keyof T as Rename<K>]: T[K] }; // After: 一旦 key だけ計算して固定、value は homomorphic を維持 type Keys<T> = { [K in keyof T]: Rename<K> }[keyof T]; type Paths<T> = { [K in Keys<T>]: ValueFor<T, K> }; // ^ 普通の mapped 、homomorphic キーを 先に確定してから、値を埋める @dOwOd · TSKaigi 2026 LT 18 / 28 § 3
  19. 型の深宇宙へ飛び込め § 5.1 · TOOLS 計測ツール ① --extendedDiagnostics terminal $

    tsc --noEmit --extendedDiagnostics ... Instantiations: 150005 ← これ! Check time: 2.13s Total time: 2.45s @dOwOd · TSKaigi 2026 LT 19 / 28 1 行追加するだけで重さの内訳が分かる tsgo も同じフラグが使える(compatible)
  20. 型の深宇宙へ飛び込め § 5.2 · TOOLS 計測ツール ② analyze-trace terminal $

    tsc --generateTrace ./trace $ npx @typescript/analyze-trace ./trace @dOwOd · TSKaigi 2026 LT 20 / 28 hot frame を炎マークで可視化 どの型が時間を食ってるか ファイル名・行番号 で出る ⚠ tsgo 7.0 Beta では まだ未対応(tsc 6.0 で取る)
  21. 型の深宇宙へ飛び込め § 6 · DIAGNOSIS 診断 3 つの 共通点 パターン

    重さの正体 再帰 · Split スタックを 積みすぎる Union · cross-product Union を 撒きすぎる Mapped · as 句あり cache を 捨てすぎる 対処 → 溜める / 包む / 分けて固める @dOwOd · TSKaigi 2026 LT 21 / 28
  22. 型の深宇宙へ飛び込め § 7 · TSC vs TSGO B E N

    C H M A R K アルゴリズムは同じ: tsc 6.0 vs tsgo 7.0 Beta パターン TSC INST TSGO INST 一致 再帰 (Split) N=900 11,732 11,732 ✓ Union (Cross-product) N=1000 150,000 150,000 ✓ Mapped ( as 句あり) K=10,000 40,016 40,016 ✓ 公式「structurally identical to TypeScript 6.0」 —— これを 実測で裏付けた スライド @dOwOd · TSKaigi 2026 LT 22 / 28
  23. 型の深宇宙へ飛び込め § 7.2 · TSGO B U T . .

    . tsgo は 速い。でも… アルゴリズムは 同じ 通常 10× tsgo speedup (typical) 重い型では 1.43× Split N=900 実測 INSTANTIATIONS = tsc と完全一致 @dOwOd · TSKaigi 2026 LT 23 / 28 tsgo は 約 10× 速い。でも、Instantiations は減らない 重い型では tsgo の 10× が 1.43× まで縮む(Split N=900 実測) → 重い型は重いまま。最終的にはあなたが書き換える
  24. 型の深宇宙へ飛び込め § 8 · TAKE-AWAY T A K E -

    A W A Y 3 行で持って帰る RECURSION 再帰は 溜める UNION Union は 包む MAPPED Mapped は 分けて固める 持ち帰る数字 3 つ CROSS-PRODUCT 75× N=1000 の改善幅 MAPPED 4,400× K=10,000 の差 TSC / TSGO 完全一致 Instantiations 同じ @dOwOd · TSKaigi 2026 LT 24 / 28
  25. 型の深宇宙へ飛び込め § 9 · ACTION A C T I O

    N 明日から、まずこれだけ package.json # package.json の scripts に 1 行 "check:diag": "tsc --noEmit --extendedDiagnostics" @dOwOd · TSKaigi 2026 LT 25 / 28 業務リポで叩いて Instantiations の桁 を見る 増えていたら、3 パターン(再帰 / Union / Mapped)のどれかを疑う 1 週間後に再実行、増えてないか確認
  26. 型の深宇宙へ飛び込め § 10 · SUMMARY まとめ 3 つのパターン / 3

    つの解決策 @dOwOd · TSKaigi 2026 LT 26 / 28 重い型には 3 つのパターン がある それぞれに 暗記フレーズの解決策 がある tsgo は速い。でも、利用されるアルゴリズムを選ぶのはあなた
  27. 型の深宇宙へ飛び込め REFERENCES 参考文献 参考文献 TypeScript Performance Wiki https://github.com/microsoft/TypeScript/wiki/Performance TypeScript Native

    Port (announcement) https://devblogs.microsoft.com/typescript/typescript-native-port/ typescript-go (tsgo) https://github.com/microsoft/typescript-go @typescript/analyze-trace https://github.com/microsoft/typescript-analyze-trace hyperfine (Rust 製ベンチ CLI) https://github.com/sharkdp/hyperfine @dOwOd · TSKaigi 2026 LT 27 / 28