この記事はAI生成によるサンプル記事です。
TypeScript の型推論について、長らく「だいたいうまく動く」程度の理解で済ませてきた。今回は推論器が実際にどう動いているのか、いくつかの典型ケースを観察しながら自分の中で言語化していきたい。
そもそも推論とは何か
型推論は、型注釈が無い場所に対して、文脈から型を導出するプロセスだ。導出は一方向ではなく、双方向(bidirectional)に行われる。引数から関数へ、関数から引数へ、戻り値から本体へ。これは Hindley-Milner からの伝統的な流れを汲んでいる。
const xs = [1, 2, 3];
// xs: number[]
const ys = xs.map((x) => x.toString());
// ys: string[]
// ↑ map のコンテキストから x: number が伝播し、
// callback の戻り値型から ys: string[] が決まる
推論の起点はどこか
推論は常に「具体側」から始まる。リテラル、変数、定数。これらが種となり、構造を辿りながら制約を伝播させていく。たとえばリテラル型の wide / narrow の判定も、変数が let なのか const なのかで分岐する。
推論できないケースを観察する
面白いのはむしろ推論が失敗するケースだ。再帰的なジェネリック、相互参照する型エイリアス、ユニオン分配のタイミング。推論器は無限ループを避けるため、深さや幅に上限を設けている。
type Last<T extends any[]> =
T extends [...infer _, infer L] ? L : never;
// この種のパターンは深さ 50 程度で打ち切られる
ここまでで「推論器は意外と単純なヒューリスティクスの集合」という印象を持った。次回はもう少し踏み込んで、Contextual typing の優先順位について書く。