26
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TypeScriptAdvent Calendar 2019

Day 4

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

Posted at

この記事は TypeScript Advent Calendar 2019 4 日目の記事です
大遅刻してしまってスミマセン・・・

3 日目の記事は @kimromi さんの Flow から TypeScript に段階的に移行する です
5 日目の記事は @hika7719 さんの AWS CDK を使って ECS(Fargate 起動タイプ)を構築する です

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

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

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

TypeScript のリポジトリの wiki に、TypeScript 本体のアーキテクチャについての概要が記載されています

Architectual Overview のページを見ると、最初に以下の図が掲載されており、ここには TypeScript の機能について、レイヤー毎の役割概観が記述されています
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 Symbols from different files, assigning Types to Symbols, and generating any semantic Diagnostics (i.e. errors).

実際にソースコード上では types.ts にて interface が定義されており、 checker.ts にて TypeChecker インスタンスを生成する関数が定義されています
ちなみに自分で TypeChecker を使う場合は wiki のUsing-the-Compiler-APIページに具体例が記載されているのでこちらを覗いてみてください

さて、上記の説明内に出てきた Program などの用語についてですが Overview のData Structures の項に記載があります
ここには TypeScript が内部的に利用しているデータ型とその役割について説明がされています
この記事でもこれらの用語を利用するので、ざっと眺めておいてもらえるといいかもしれません

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

ここまで Overview のページを見てきましたが、実は内部実装について他にも説明しているページがあります
Compiler Internals
情報は少ないですが、Checker という項で TypeChecker について説明がされておりますが、今回の内容とは直接関係ないので割愛します

TypeChecker の設計

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

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

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


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

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

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

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

チェック処理の例として、例えば対象の Node が InferType ( type X = Array<string> extends Array<infer T> ? T : neverinfer 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
index.ts
const x: number = '';

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


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

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

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 という関数が呼び出されています

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

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

checkDeferredNodes(node);
// 略

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

// 略
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 になるので、その分岐を探っていきますが、コードを追うと型チェック自体は VariableDeclarationNode に対して行われることになるので、 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:numbernumber='''' の 2 つの型が比較され、型が不一致のためにエラーという流れになっています

おわりに

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

26
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?