tsc の全体像 — 5段パイプライン
TypeScriptコンパイラ(tsc)は、ソースコードをJavaScriptに変換する過程で
5つの主要コンポーネントを順に通過させます。
このパイプラインの理解は、TypeScriptの型推論がなぜそのように動くのか、
エラーメッセージがなぜそのような形になるのかを理解する鍵です。
graph LR A[SourceCode\n.ts ファイル] --> B[Scanner\n字句解析] B -->|Token Stream| C[Parser\n構文解析] C -->|AST| D[Binder\n束縛] D -->|AST + Symbols| E[Checker\n型チェック] E -->|Validated AST| F[Emitter\n出力] F --> G[.js ファイル] F --> H[.d.ts ファイル] style A fill:#6b7280,stroke:#4b5563,color:#fff style B fill:#8b5cf6,stroke:#7c3aed,color:#fff style C fill:#8b5cf6,stroke:#7c3aed,color:#fff style D fill:#8b5cf6,stroke:#7c3aed,color:#fff style E fill:#ef4444,stroke:#dc2626,color:#fff style F fill:#8b5cf6,stroke:#7c3aed,color:#fff style G fill:#22c55e,stroke:#16a34a,color:#fff style H fill:#22c55e,stroke:#16a34a,color:#fff
Scanner — ソースコードをトークンに分解
Scannerはソースコードの文字列をトークン(Token)の列に変換します。 トークンとは、言語の最小単位(キーワード、識別子、演算子、リテラルなど)です。
// 入力: TypeScriptソースコード
const age: number = 25;
// ↓ Scanner の出力(トークン列)
// [ConstKeyword] [Identifier:"age"] [ColonToken]
// [NumberKeyword] [EqualsToken] [NumericLiteral:25]
// [SemicolonToken] Scannerはステートフルであり、Parserから呼び出される形で動作します。 診断情報(エラー・警告)の生成も担当しており、 構文的に不正なトークン(閉じられていない文字列リテラルなど)はこの段階で検出されます。
Parser — トークンをASTに変換
Parserはトークン列をAST(Abstract Syntax Tree / 抽象構文木)に変換します。 ASTはコードの構造をツリー形式で表現したもので、後続の全処理の基盤となります。
// 入力コード
const x = a + 1;
// ↓ AST の構造(簡略化)
// VariableStatement
// └── VariableDeclaration
// ├── Identifier: "x"
// └── BinaryExpression
// ├── Identifier: "a"
// ├── PlusToken
// └── NumericLiteral: 1
各ASTノードはSyntaxKind(ノードの種類)、ソースコード上の位置情報、子ノードへの参照を持ちます。
このASTは後続の全フェーズ(Binder、Checker、Emitter)で共有・利用されます。
Binder — 宣言とスコープを結びつける
BinderはASTを走査し、変数・関数・クラスなどの宣言ごとにSymbol(シンボル)を作成します。 シンボルはスコープ内のSymbolTableに格納され、「この名前がこのスコープで何を指すか」を記録します。
graph TD A[Binder の役割] --> B[Symbol 作成\n各宣言にSymbolを付与] A --> C[SymbolTable 構築\nスコープごとに名前→Symbol] A --> D[制御フローグラフ構築\n型ナローイング用] A --> E[宣言マージ判定\nSymbolFlagsで制御] style A fill:#3b82f6,stroke:#2563eb,color:#fff style B fill:#8b5cf6,stroke:#7c3aed,color:#fff style C fill:#8b5cf6,stroke:#7c3aed,color:#fff style D fill:#ef4444,stroke:#dc2626,color:#fff style E fill:#8b5cf6,stroke:#7c3aed,color:#fff
Binderの最も重要な仕事は制御フローグラフ(Control Flow Graph / CFG)の構築です。 これは、コードの実行パスを表す有向グラフで、後続のCheckerが型ナローイング(型の絞り込み)を行う際に使用します。
また、BinderはSymbolFlags(ビットフラグ)を使って、宣言のマージ可能性を制御します。
例えば、TypeScriptではinterfaceは同名宣言のマージ(Declaration Merging)が可能ですが、
classは不可能です。この判定がBinderで行われます。
Checker — コンパイラの心臓部
Checker(型チェッカー)はTypeScriptコンパイラの最大かつ最重要のコンポーネントです。
ソースコードのchecker.tsは50,000行を超える巨大なファイルで、
型推論、型互換性チェック、ジェネリクスの解決、制御フロー解析による型ナローイングを実行します。
遅延評価方式 — 必要な時だけ計算する
Checkerは遅延評価(Lazy Evaluation)方式を採用しています。 すべての型を先に計算するのではなく、必要になった時点で初めて型を計算します。 これにより、使われない変数や到達不能なコードの型計算を省略でき、パフォーマンスが向上します。
制御フロー解析 — 逆方向トラバーサル
TypeScriptの型推論で最も洗練された部分が、制御フロー解析(Control Flow Analysis)です。
これにより、if文やtypeofチェックの後で型が自動的に絞り込まれます。
function example(value: string | number) {
// ここでは value は string | number
if (typeof value === "string") {
// ここでは value は string に絞り込まれる
console.log(value.toUpperCase()); // OK
} else {
// ここでは value は number に絞り込まれる
console.log(value.toFixed(2)); // OK
}
}
内部的には、Binderが構築した制御フローグラフをCheckerが逆方向に辿ることで型を決定します。
中核関数getFlowTypeOfReference(1,200行超)が、変数の参照地点から逆方向にフローノードを辿り、
途中で型を絞り込みながら最終的な型を算出します。
graph TD
A[変数宣言\nvalue: string ∣ number] --> B{typeof === string?}
B -->|true| C[value: string\n型がナローイング]
B -->|false| D[value: number\n型がナローイング]
C --> E[value.toUpperCase\n✅ string のメソッド]
D --> F[value.toFixed\n✅ number のメソッド]
style A fill:#6b7280,stroke:#4b5563,color:#fff
style B fill:#eab308,stroke:#ca8a04,color:#000
style C fill:#22c55e,stroke:#16a34a,color:#fff
style D fill:#22c55e,stroke:#16a34a,color:#fff
style E fill:#3b82f6,stroke:#2563eb,color:#fff
style F fill:#3b82f6,stroke:#2563eb,color:#fffCheckerの主要なナローイング関数
| 関数名 | 処理内容 | 対応する構文 |
|---|---|---|
| narrowTypeByTruthiness | 真偽値チェックによる絞り込み | if (value) |
| narrowTypeByTypeof | typeof演算子による絞り込み | typeof x === "string" |
| narrowTypeByEquality | 等値比較による絞り込み | x === null |
| narrowTypeBySwitchOnDiscriminant | 判別共用体のswitch文 | switch (shape.kind) |
| narrowTypeByInstanceof | instanceofによる絞り込み | x instanceof Date |
| narrowTypeByIn | in演算子による絞り込み | "name" in obj |
Emitter — 型を消去してJavaScriptを出力
Emitterは、型チェック済みのASTからJavaScript(.js)と型定義ファイル(.d.ts)を出力します。 このフェーズでType Erasureが行われ、型注釈・インターフェース・型エイリアスなど、 TypeScript固有の構文はすべて除去されます。
// TypeScript ソースコード
interface Config {
port: number;
host: string;
}
function startServer(config: Config): void {
console.log(`Starting on ${config.host}:${config.port}`);
}
// ↓ Emitter の出力(.js)— 型は完全に消去
function startServer(config) {
console.log(`Starting on ${config.host}:${config.port}`);
}
// ↓ Emitter の出力(.d.ts)— 型情報のみ保持
interface Config {
port: number;
host: string;
}
declare function startServer(config: Config): void; 意図的な7つの非健全性
第1章で触れた通り、TypeScriptは「健全な型システム」をNon-Goalとしています。 具体的には、以下の7つの箇所で意図的に非健全性が許容されています。
| 非健全性 | 内容 | 対策 |
|---|---|---|
any型 | 型チェックを完全に無効化するエスケープハッチ | unknownを使う |
型アサーション(as) | 開発者が型を強制的に上書き可能 | satisfiesを優先 |
| 配列のインデックスアクセス | 範囲外アクセスがundefinedにならない | noUncheckedIndexedAccess |
| 不正確な型定義(.d.ts) | ランタイムとの不一致の可能性 | DefinitelyTypedの品質に依存 |
| 共変配列 | Dog[]をAnimal[]に代入するとCatが混入可能 | 設計上の妥協 |
| 関数呼び出し後の型保持 | 関数がオブジェクトを変更しても型が保持される | 設計上の妥協 |
| void型の扱い | 一部のケースで型の不整合が発生 | 稀なケース |
class Animal { name = "animal"; }
class Dog extends Animal { bark() { console.log("woof"); } }
class Cat extends Animal { meow() { console.log("meow"); } }
const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs; // OK: Dog[] は Animal[] に代入可能(共変)
animals.push(new Cat()); // OK: Cat は Animal なので代入可能
dogs[1].bark(); // 実行時エラー! dogs[1] は Cat なので bark() がない 現代のビルドアーキテクチャ — tscは型チェック専用に
2026年の現代的なTypeScriptプロジェクトでは、tscは型チェック専用で使い、
JavaScript出力はSWCやesbuildなどの高速ツールに任せるのが標準パターンです。
graph TD A[.ts ソースコード] --> B[tsc --noEmit\n型チェックのみ] A --> C[SWC / esbuild\n高速トランスパイル] B --> D[型エラーの検出\n✅ or ❌] C --> E[.js 出力\nバンドル済み] style A fill:#6b7280,stroke:#4b5563,color:#fff style B fill:#3b82f6,stroke:#2563eb,color:#fff style C fill:#22c55e,stroke:#16a34a,color:#fff style D fill:#8b5cf6,stroke:#7c3aed,color:#fff style E fill:#22c55e,stroke:#16a34a,color:#fff
| ツール | 言語 | 役割 | tsc比の速度 |
|---|---|---|---|
| tsc | TypeScript→Go (v7) | 型チェック + 出力 | 1x(基準) |
| esbuild | Go | トランスパイル + バンドル | 約45倍 |
| SWC | Rust | トランスパイル | 約20倍 |
| tsgo (TS 7.0) | Go | 型チェック + 出力 | 約10倍(tsc比) |
理解度チェック
TypeScriptコンパイラの5段パイプラインを正しい順序に並べてください。
矢印ボタンで正しい順序に並べ替えてください