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
TypeScriptコンパイラの5段パイプライン — Checker(赤)が最大のコンポーネント

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の4つの役割 — 特に制御フローグラフの構築がCheckerの型ナローイングに直結する

Binderの最も重要な仕事は制御フローグラフ(Control Flow Graph / CFG)の構築です。 これは、コードの実行パスを表す有向グラフで、後続のCheckerが型ナローイング(型の絞り込み)を行う際に使用します。

また、BinderはSymbolFlags(ビットフラグ)を使って、宣言のマージ可能性を制御します。 例えば、TypeScriptではinterfaceは同名宣言のマージ(Declaration Merging)が可能ですが、 classは不可能です。この判定がBinderで行われます。

Checker — コンパイラの心臓部

Checker(型チェッカー)はTypeScriptコンパイラの最大かつ最重要のコンポーネントです。 ソースコードのchecker.ts50,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:#fff
制御フロー解析による型ナローイング — typeof チェックの後で型が自動的に絞り込まれる

Checkerの主要なナローイング関数

関数名 処理内容 対応する構文
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)とトランスパイル(SWC/esbuild)の分離
ツール 言語 役割 tsc比の速度
tsc TypeScript→Go (v7) 型チェック + 出力 1x(基準)
esbuild Go トランスパイル + バンドル 約45倍
SWC Rust トランスパイル 約20倍
tsgo (TS 7.0) Go 型チェック + 出力 約10倍(tsc比)

理解度チェック

問題 0 / 50%
Q1

TypeScriptコンパイラの5段パイプラインを正しい順序に並べてください。

矢印ボタンで正しい順序に並べ替えてください

1Binder
2Checker
3Parser
4Emitter
5Scanner