TypeScript の tsc コマンドを叩いたときに const x:number = ''; がコンパイルエラーになるまでの道のり

この記事は [TypeScript Advent Calendar 2019](https://qiita.com/advent-calendar/2019/typescript) 4 日目の記事です
大遅刻してしまってスミマセン・・・

3 日目の記事は @kimromi さんの [Flow から TypeScript に段階的に移行する](https://kimromi.hatenablog.jp/entry/2019/12/03/134214) です
5 日目の記事は @hika7719 さんの [AWS CDK を使って ECS(Fargate 起動タイプ)を構築する](https://miyahara.hikaru.dev/posts/20191205/) です

# TypeScript の tsc コマンドを叩いたときに const x:number = ''; がコンパイルエラーになるまでの道のり

TypeScript がどのようにして型チェックをしているのかとふと疑問に思ったのでコードを読んでみました
実際に tsc コマンドを叩いてから `const x:number = '';` という「数字型の変数宣言に文字列の値を代入」するコードがコンパイルエラーとして検出されるまでの流れを一通り読んでみたので、解説してみようと思います

## TypeScript のアーキテクチャ概要について

[TypeScript のリポジトリの wiki](https://github.com/microsoft/TypeScript/wiki) に、TypeScript 本体のアーキテクチャについての概要が[記載されています](https://github.com/microsoft/TypeScript/wiki/Architectural-Overview)

Architectual Overview のページを見ると、最初に以下の図が掲載されており、ここには TypeScript の機能について、レイヤー毎の役割概観が記述されています
![architecture.png](https://raw.githubusercontent.com/wiki/Microsoft/TypeScript/images/architecture.png)

当たり前ですが、型チェックはこのうち `Core TypeScript Compiler` という最も低レイヤの位置にありますね
図のすぐ下には `Core TypeScript Compiler` の各種機能についての概要があり、これによると `Type resolver/Checker` が型チェックを担っていることがわかります

また、 `Overview of the compilation process` の項目に型システムについて以下のように記載されており、 `TypeChecker` が TypeScript の型システムのコアであることがわかります

> From a `Program` instance a `TypeChecker` can be created. `TypeChecker` is the core of the TypeScript type system. It is the part responsible for figuring out relationships between `Symbol`s from different files, assigning `Type`s to `Symbol`s, and generating any semantic `Diagnostic`s (i.e. errors).

実際にソースコード上では [types.ts](https://github.com/microsoft/TypeScript/blob/09271f107d41c91836b196c4e28d1ddd692d8ebc/src/compiler/types.ts#L3291) にて interface が定義されており、 [checker.ts](https://github.com/microsoft/TypeScript/blob/09271f107d41c91836b196c4e28d1ddd692d8ebc/src/compiler/checker.ts#L260) にて TypeChecker インスタンスを生成する関数が定義されています
ちなみに自分で TypeChecker を使う場合は wiki の[Using-the-Compiler-API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API)ページに具体例が記載されているのでこちらを覗いてみてください

さて、上記の説明内に出てきた `Program` などの用語についてですが Overview の[Data Structures](https://github.com/microsoft/TypeScript/wiki/Architectural-Overview#data-structures) の項に記載があります
ここには TypeScript が内部的に利用しているデータ型とその役割について説明がされています
この記事でもこれらの用語を利用するので、ざっと眺めておいてもらえるといいかもしれません

\*補足
Data Structures には説明がないですが、TypeScript ではエラーメッセージを表現するデータ型として `Diagnostic` 型があるのでこちらで簡単に説明します
`Diagnostic` 型は構文エラーや文法エラー、型エラーを始めとしたコンパイルエラー全般だけでなく tsconfig の設定エラーなども含んだ汎用のエラーメッセージを表すデータ型です
TypeChecker 内での型チェック処理においても、型エラーが検出されたらこの Diagnostic インスタンスとしてエラー情報を保持しています

ここまで Overview のページを見てきましたが、実は内部実装について他にも説明しているページがあります
[Compiler Internals](https://github.com/microsoft/TypeScript/wiki/Compiler-Internals)
情報は少ないですが、Checker という項で TypeChecker について説明がされておりますが、今回の内容とは直接関係ないので割愛します

## TypeChecker の設計

\*自分がコードを読んで得た情報から考えたことなので、実態にそぐわないかもしれません、あしからずご了承ください

`TypeChecker` は `Program` を元に生成されます。つまり、型チェックの対象となる TypeScript コード・tsconfig を含めた TypeScript プロジェクト全体に対して一意の型チェック機構が生成されると言えるでしょう
例えば tsconfig の `strictNullChecks` といった型チェックに関連する設定が異なれば当然 `TypeChecker` の振る舞いも変わるといった具合です
また、 `Program` 型は型チェック以外にも構文チェックなどの他のチェック処理関数も実装されています
`TypeChecker` はこの`Program` が持つチェック処理のうちの一つと捉えられそうです。ちなみに他には構文チェック、コンフィグチェックなどがあります
[このへん](https://github.com/microsoft/TypeScript/blob/64a886700fa0a6f49abb038b1826840b000d5961/src/compiler/watch.ts#L155)でそれらのチェック処理が実施されていますね（ここは tsc を実行した際に呼ばれる場所。つまり、tsc 実行時に標準出力に表示されるエラー全般がつくられている）
型チェック処理が `Program` の関数の一つとして実装されているということは、各種チェック処理をどういう順番で行うかは呼び出し側に委ねられているといえます。コンパイラ（tsc）と LanguageService という複数の呼び出しパスがありえるためにこのような実装になっているのでしょう

さて、 `Program` に対して生成される `TypeChecker` ですが、型チェック処理の実行は `SourceFile` （≒d.ts,ts ファイル）単位となります
`LanguageService` から呼び出されるときは単一ファイルに対して型チェックされるように、といった使い分けのためですね
`Program` 全体に対して型チェックをしたい場合（tsc の実行時など）は `Program` が持つ `SourceFile` のインスタンス全てに対してチェック処理を実施、という風に[実装されて](https://github.com/microsoft/TypeScript/blob/64a886700fa0a6f49abb038b1826840b000d5961/src/compiler/program.ts#L1647)います
もちろんファイル横断のエラーも存在しますが、ファイル単位のエラーとは別の変数で `TypeChecker` インスタンス内に保持されます

---

ここまでは `Program` と `TypeChecker` の関係、 `TypeChecker` がどんな単位で実行されるのかを説明しました
では実際に `TypeChecker` はどのようにして型チェックをしているのかを説明していきます

といっても AST Node を上から下まで舐めて、愚直に式や文から型を判定して、最終的に比較すべき型を拾い上げて比較し、型が正しいかどうかを判定しているだけです
TypeScript の型の表現力はやたらめったら豊かなのでチェック関数はめちゃくちゃ複雑ですが・・・

もう少し具体的な話をします

`TypeChecker` 内には `check**`という命名規則で各種型チェック関数が定義されています
この `check**` 関数は基本的には対象の Node の型などが正しいかどうかを判定し、エラーなら Diagnostic を生成して配列へ格納します
実際にエラーを出力する箇所ではこの配列を参照して全てのエラーメッセージを表示する、といった処理を行うことになりますね
（ちなみにデバッガでこの Diagnostic を持つ配列が参照しにくいため、いつ型エラーが検出されたか？を確認するのは結構骨が折れました・・・）

チェック処理の例として、例えば対象の Node が `InferType` ( `type X = Array<string> extends Array<infer T> ? T : never` の `infer T` の部分)だった場合は、 `checkInferType` という関数を呼び出して、 `infer` キーワードがちゃんと `Conditional Types` の文脈で呼ばれているかなどの判定がされることになります
これを非常に多岐にわたる Node の種類に対して個別に実装している、というのが `TypeChecker` です。すごい

個人的には今回コードを読んでいて、 `isSimpleTypeRelatedTo` という関数で `if (s & TypeFlags.Undefined && (!strictNullChecks || t & (TypeFlags.Undefined | TypeFlags.Void))) return true;` という処理がずらずら並んだ泥臭いコードを読めたので満足しています（？）

ちなみに `TypeChecker` はパフォーマンスが求められるため、処理の中断のための `CancellationToken` だったり、恐らく race condition 対策と思われるコード、遅延評価になるような実装が散りばめられているように見受けられましたが、筆者はそれらは読んでいない（読み解けるかもわからない）です。。無念

## tsc 呼び出しから型チェックが実施されるまでの流れ

さてここまで長々と内部実装の概要を説明してきましたが、ようやく実際に tsc コマンド実行からの流れを追っていきます

なお、今回対象となるディレクトリ構成は以下のようなものとします

```
root
+-- src/
|    +-- index.ts
+-- tsconfig.json
```

```ts:index.ts
const x: number = '';
```

この `root` ディレクトリ配下で `npx tsc` コマンドを実行したものとします
サンプルリポジトリ: [https://github.com/sisisin-sandbox/dive-checker](https://github.com/sisisin-sandbox/dive-checker)

---

tsc のエントリポイントは [tsc.ts](https://github.com/microsoft/TypeScript/blob/64a886700fa0a6f49abb038b1826840b000d5961/src/tsc/tsc.ts) で、ここから順を追ってコードを読んでいきます
`executeCommandLine.ts` に飛びますが、ここは要はコマンドラインインターフェースを定義して引数に応じて Core TypeScript Compiler の呼び出しをしているだけです
今回の場合、引数なしで tsc コマンドを実行しているので [executeCommandLineWorker](https://github.com/microsoft/TypeScript/blob/64a886700fa0a6f49abb038b1826840b000d5961/src/tsc/executeCommandLine.ts#L170) 関数から [performCompilation](https://github.com/microsoft/TypeScript/blob/64a886700fa0a6f49abb038b1826840b000d5961/src/tsc/executeCommandLine.ts#L493) 関数を呼び出してコンパイル処理に入ります
`performCompilation` 内で `emitFilesAndReportErrorsAndGetExitStatus` といういかにも型チェックなどの処理をやっていそうな関数を呼び出しているので中を覗くと [emitFilesAndReportErrors 関数の返り値を emitResult と diagnostics という変数に代入しています](https://github.com/microsoft/TypeScript/blob/64a886700fa0a6f49abb038b1826840b000d5961/src/compiler/watch.ts#L210)
[emitFilesAndReportErrors](https://github.com/microsoft/TypeScript/blob/64a886700fa0a6f49abb038b1826840b000d5961/src/compiler/watch.ts#L142) の中では `Program` インスタンスの持つチェック処理を呼び出していますね。この中の `getSemanticDiagnostics` 呼び出しが今回探している型チェック処理になります

続けて読み進めます
`Program` インスタンスは別途 interface が定義されている関係で定義ジャンプで飛べません。 `program.ts` ファイルを直接開いて関数名でファイル内検索して [getSemanticDiagnostics](https://github.com/microsoft/TypeScript/blob/64a886700fa0a6f49abb038b1826840b000d5961/src/compiler/program.ts#L1659)の実装を探します（正攻法は `performCompilation` の実装まで戻って `const program = createProgram(programOptions);` から辿る、だとは思います）
ヘルパなどが用意されていますが、読み進めていくと実態は[getSemanticDiagnosticsForFileNoCache](https://github.com/microsoft/TypeScript/blob/64a886700fa0a6f49abb038b1826840b000d5961/src/compiler/program.ts#L1713) であることがわかります
この `getSemanticDiagnosticsForFileNoCache` 内で以下のように `Diagnostcs` を作りまくっているところにいますね、 `typeChecker.getDiagnostics()`という呼び出しが。`TypeChecker` のおでましです

```ts
const bindDiagnostics: readonly Diagnostic[] = includeBindAndCheckDiagnostics ? sourceFile.bindDiagnostics : emptyArray;
const checkDiagnostics = includeBindAndCheckDiagnostics ? typeChecker.getDiagnostics(sourceFile, cancellationToken) : emptyArray;
const fileProcessingDiagnosticsInFile = fileProcessingDiagnostics.getDiagnostics(sourceFile.fileName);
const programDiagnosticsInFile = programDiagnostics.getDiagnostics(sourceFile.fileName);
```

\* checker.ts はデカすぎて GitHub で見れないのでここからはリンクは張りません

`TypeChecker` も interface が定義されているので、これまた実装を `checker.ts` ファイルから直接探します
`getDiagnosticsWorker` 関数内に `checkSourceFile(sourceFile);` という呼び出しがありますが、こいつが `SourceFile` に対しての型チェック処理になります
`checkSourceFileWorker` が実態ですね。中では色々なチェック関数が呼び出されていますが、 `SourceFile` 内の各文に対して `checkSourceElements` という関数が呼び出されています

```ts
// 略
clear(potentialThisCollisions);
clear(potentialNewTargetCollisions);

forEach(node.statements, checkSourceElement);
checkSourceElement(node.endOfFileToken);

checkDeferredNodes(node);
// 略
```

この `checkSourceElement` に飛び、その先の `checkSourceElementWorker` に飛ぶと、いました。 `Node` の `SyntaxKind` に応じてチェック処理を実施している場所です
ちなみにこの `checkSourceElementWorker` 内の処理中にまた `checkSourceElementWorker` を呼び出す再帰構造になっています

```ts
// 略
switch (kind) {
    case SyntaxKind.TypeParameter:
        return checkTypeParameter(<TypeParameterDeclaration>node);
    case SyntaxKind.Parameter:
        return checkParameter(<ParameterDeclaration>node);
    case SyntaxKind.PropertyDeclaration:
    case SyntaxKind.PropertySignature:
        return checkPropertyDeclaration(<PropertyDeclaration | PropertySignature>node);
// 略
```

呼び出し元の実装（ `SourceFile` に紐づく全ての文に対してこの関数を呼び出す）からもわかるように、この `checkSourceElementWorker` が型チェックのキモのようです
型チェックの多くはここを起点に行われます

さて、今回目指すのは `const x:number = '';`という文です
これは `VariableStatement` になるので、その分岐を探っていきますが、コードを追うと型チェック自体は `VariableDeclaration` の `Node` に対して行われることになるので、 `checkVariableDeclaration` を見に行きます
が、ここから先はややこしいところが続くので、チェック処理として呼び出される関数名だけ並べていきます

- checkVariableDeclaration
- checkVariableLikeDeclaration
- checkTypeAssignableToAndOptionallyElaborate
- checkTypeRelatedToAndOptionallyElaborate
- checkTypeRelatedTo
- isRelatedTo
- reportRelationError
  - ここで `Diagnostics.Type_0_is_not_assignable_to_type_1` というメッセージが指定される
- reportError
  - これがゴール。 `reportRelationError` で指定されたテンプレートを元に、 `"" is not assignable to type number` というメッセージが生成される

最終的には `isRelatedTo` という関数にて `x:number` の `number` と `=''` の`''` の 2 つの型が比較され、型が不一致のためにエラーという流れになっています

# おわりに

長くなりましたが、 `const x:number = '';` がコンパイルエラーになるまでの道のりを追ってみました
今回は非常に簡単な型のチェックだったのでなんとか追い切れましたが、複雑な型になるととても読み切れる気がしないですね。。
ですが、個人的には TypeScript がどうやって型チェックしているかを追う事ができたので満足しています
また何かあったら本体のコードを細かく読んでみたい所存です