Slide 1

Slide 1 text

1 / 33 決定論的な型チェックへ Go 製コンパイラによる10倍速の裏側で --stableTypeOrdering から見える並列化への挑戦 TSKaigi 2026 Copyright dip.inc All Rights Reserved.

Slide 2

Slide 2 text

自己紹介 えーたろー ディップ株式会社 / バックエンド・SRE X: @komugi8448 最近のハマり事: たべること Copyright dip.inc All Rights Reserved. 2 / 33

Slide 3

Slide 3 text

目次 Project Corsa と TypeScript 6.0 / 7.0 1 .d.ts の順序問題 2 コンパイラ内部を覗く 3 並列化下の type ID 採番 4 --stableTypeOrdering と compareTypes 5 使うときの注意と移行手順 6 まとめ 7 Copyright dip.inc All Rights Reserved. 3 / 33

Slide 4

Slide 4 text

01 Project Corsa と TypeScript 6.0 / 7.0

Slide 5

Slide 5 text

Project Corsa とは 概要 TypeScript コンパイラを Go へ移植 する 公式プロジェクト (2025 年 Anders Hejlsberg 発表) ビルド時間 約 10 倍 高速化、エディタ起 動・メモリ使用量も 大幅改善 バージョンの位置づけ TypeScript 7.0 としてリリース予定 その直前、最後の JavaScript ベース版 が今日の主役の TypeScript 6.0 Copyright dip.inc All Rights Reserved. 5 / 33

Slide 6

Slide 6 text

TypeScript 6.0 で導入された新しいコンパイルフラグ。7.0 への移行をスムーズにするた めの仕掛け の一つ。 進め方 本日の主題 --stableTypeOrdering TypeScript 6.0 の振る舞い をソースから読み解く 1 並列化 × type ID で何が起きるか を考える 2 --stableTypeOrdering の正体 をソースから読み解く 3 Copyright dip.inc All Rights Reserved. 6 / 33

Slide 7

Slide 7 text

02 .d.ts の順序問題

Slide 8

Slide 8 text

export function foo(condition: boolean) { return condition ? 100 : 500; } tsc --declaration --emitDeclarationOnly で型定義を生成 export declare function foo(condition: boolean): 100 | 500; 既存コンパイラの特徴 Copyright dip.inc All Rights Reserved. 8 / 33

Slide 9

Slide 9 text

const x = 500; // foo とは何の関係もない宣⾔ export function foo(condition: boolean) { return condition ? 100 : 500; } export declare function foo(condition: boolean): 500 | 100; Union の順序が逆転 const を一行追加してみる Copyright dip.inc All Rights Reserved. 9 / 33

Slide 10

Slide 10 text

03 コンパイラ内部を覗く

Slide 11

Slide 11 text

checker.ts より: function createType(flags: TypeFlags): Type { const result = new Type(checker, flags); typeCount++; result.id = typeCount; // ... } createType : 型を作るたびに ID を採番 新しい型を作るたびに typeCount をインクリメント → その値をそのまま型の ID に 生成順 = ID の昇順 Copyright dip.inc All Rights Reserved. 11 / 33

Slide 12

Slide 12 text

function addTypeToUnion(typeSet: Type[], includes: TypeFlags, type: Type) { // ... 'never' 判定や flag 集計などは省略 ... const index = binarySearch(typeSet, type, getTypeId, compareValues); if (index < 0) { typeSet.splice(~index, 0, type); } return includes; } addTypeToUnion : Union に要素を追加する binarySearch = 二分探索 (O(log n) で挿入位置を発見) 比較キー: getTypeId / 比較関数: compareValues つまり「type ID を取り出して、数値として比較」 Copyright dip.inc All Rights Reserved. 12 / 33

Slide 13

Slide 13 text

逆転現象の正体 const x = 500; を一行追加すると その行を処理する過程で 500 のリテラル型が先に生成され ID を先取り 結果、リテラル型 100 と 500 の ID の大小関係が逆転 ( ID(500) < ID(100) ) ID 昇順ソートの Union の並びも逆転 ( 100 | 500 → 500 | 100 ) Copyright dip.inc All Rights Reserved. 13 / 33

Slide 14

Slide 14 text

04 並列化下の type ID 採番

Slide 15

Slide 15 text

シングルスレッドだから安定していた いまの TypeScript (JS 版) JavaScript はシングルスレッド 型が作られる順序は 毎回同じ だから ID も毎回同じ → 出力も安定 TypeScript 7.0 (Go 版) 軽量な並行モデル (goroutine) 既存 TS 実装から ほぼ行単位で移植 で きる構造 複数のファイルを 並列にチェック Copyright dip.inc All Rights Reserved. 15 / 33

Slide 16

Slide 16 text

その結果 並列化 × type ID で起きること どの goroutine が先に ID を取るかは 実行順序が非決定的 実行のたびに 同じ型に違う ID が振られる 結果として .d.ts の出力順が 毎回変わる ビルドの 非決定性: 同じソースから違う成果物 公開済み .d.ts に意味のない diff が出続ける Copyright dip.inc All Rights Reserved. 16 / 33

Slide 17

Slide 17 text

05 --stableTypeOrdering と compareTypes

Slide 18

Slide 18 text

const index = stableTypeOrdering ? binarySearch(typeSet, type, identity, compareTypes) // 内容ベース : (len && type.id > typeSet[len - 1].id ? ~len : binarySearch(typeSet, type, getTypeId, compareValues)); // ID ベース if (index < 0) { typeSet.splice(~index, 0, type); } addTypeToUnion 内のフラグ分岐 Copyright dip.inc All Rights Reserved. 18 / 33

Slide 19

Slide 19 text

compareTypes : コンテンツベースの総合比較 TypeScript 7.0 で新しく追加された関数 任意の 2 つの型を受け取って、どちらが先かを決める Union 構成要素の並べ替えなど、出力順に関わる場面で広く使用 採番順ではなく 型そのものの「内容」 を見て決定論的に並び替える Copyright dip.inc All Rights Reserved. 19 / 33

Slide 20

Slide 20 text

複数のキーを優先度の高い順に見ていって、決着がついたところで止まる 優先度 段階的なキー比較 型のフラグ (種類) 1 型の名前 2 型ごとの固有情報 3 (最後の手段) type ID 4 Copyright dip.inc All Rights Reserved. 20 / 33

Slide 21

Slide 21 text

function compareTypes(t1, t2): number { if (t1 === t2) return 0; if (t1 === undefined) return -1; if (t2 === undefined) return 1; // First sort in order of increasing type flags values. let c = getSortOrderFlags(t1) - getSortOrderFlags(t2); if (c !== 0) return c; // 以降、キー 2 以降の⽐較が続く } TypeFlags.String / Object / Union … の数値を引き算。差がついたら即 return 。 キー 1: 型のフラグ (種類) Copyright dip.inc All Rights Reserved. 21 / 33

Slide 22

Slide 22 text

// Order named types by name and, in the case of aliased types, // by alias type arguments. c = compareTypeNames(t1, t2); if (c !== 0) return c; // 以降、キー 3 以降の⽐較が続く キー 2: 型の「名前」 型エイリアスならその名前、クラス / インターフェースならシンボル名 文字列として比較 同じ「型の種類」の中をさらに名前で並べる Copyright dip.inc All Rights Reserved. 22 / 33

Slide 23

Slide 23 text

else if (t1.flags & TypeFlags.Object) { const c = compareSymbols(t1.symbol, t2.symbol); // 宣⾔位置 / 型引数 // 以降、型引数列など追加の⽐較が続く } else if (t1.flags & TypeFlags.Union) { // 構成要素のリストを再帰的に⽐較 } else if (t1.flags & TypeFlags.StringLiteral) { // リテラルの値そのもので⽐較 } // 他にも Intersection / Enum / NumberLiteral / TypeParameter などのケースが続く それぞれの型の 「中身」 にまで踏み込んで比較。 キー 3: 型ごとの「固有情報」 Copyright dip.inc All Rights Reserved. 23 / 33

Slide 24

Slide 24 text

// Fall back to type IDs. // This results in type creation order for built-in types. return t1.id - t2.id; 最後の手段: type ID へのフォールバック ここまで差がつかなかったときだけ ID を使う 主に any / unknown など組み込み型を安定化するためのフォールバック ユーザー定義の型同士でここまで落ちてくることはほぼない Copyright dip.inc All Rights Reserved. 24 / 33

Slide 25

Slide 25 text

TypeScript 7.0 で採用される並び順を、ひと足先に 6.0 で体験できる。事前に有効化す れば、7.0 移行時に並びが突然変わる事故を防げる。 フラグの正体 true のとき キーセレクタ: identity (= 型そのもの) 比較関数: compareTypes → コンテンツベース のソート false のとき キーセレクタ: getTypeId 比較関数: compareValues → 従来通りの ID ベース のソート Copyright dip.inc All Rights Reserved. 25 / 33

Slide 26

Slide 26 text

06 使うときの注意と移行手順

Slide 27

Slide 27 text

型エラーは 明示的な型注釈 で潰す 注意点と対処 コスト 最大 25% のチェック速度低下 (コンテンツソート + 6.0 は並列化なし) 公式: 「常時使うことは推奨しない」 → 一時利用 前提 対処 Copyright dip.inc All Rights Reserved. 27 / 33

Slide 28

Slide 28 text

まとめ

Slide 29

Slide 29 text

まとめ: 今日の 3 ポイント TypeScript コンパイラは 型ごとに一意な数値の ID で管理し、Union の並び順は採番 順に依存 1 Go 移植版 (TS 7.0) では 並列化により ID 採番順が非決定的 になる 2 これを解決するため compareTypes による コンテンツベースの決定論的な多段キー 比較 に切り替え、TS 6.0 では --stableTypeOrdering フラグで先取り体験できる 3 Copyright dip.inc All Rights Reserved. 29 / 33

Slide 30

Slide 30 text

参考文献: ドキュメント A 10x Faster TypeScript https://devblogs.microsoft.com/typescript/typescript-native-port/ TypeScript 6.0 リリースノート https://www.typescriptlang.org/docs/handbook/release-notes/typescript-6-0.html Why Go? https://github.com/microsoft/typescript-go/discussions/411#discussioncomment- 12466988 Copyright dip.inc All Rights Reserved. 30 / 33

Slide 31

Slide 31 text

参考文献: ソースコード microsoft/typescript https://github.com/microsoft/TypeScript microsoft/typescript-go https://github.com/microsoft/typescript-go Copyright dip.inc All Rights Reserved. 31 / 33

Slide 32

Slide 32 text

参考文献: 型定義 publish 関連 TS Handbook: Publishing https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html TS tsconfig: declaration option https://www.typescriptlang.org/tsconfig/#declaration Copyright dip.inc All Rights Reserved. 32 / 33

Slide 33

Slide 33 text

ありがとうございました Enjoy your TypeScript life! Copyright dip.inc All Rights Reserved.