判別可能ユニオンで状態をモデル化し、never 型でその網羅性を固定します。
このレッスンを Kodokon で開くnever は空の型です。どんな値もそこには存在しません。コードパスが到達不可能であるとコンパイラが証明したときに現れます。たとえば、ユニオンのすべてのメンバーを使い尽くした後などです。この性質が never を設計ツールにします。switch の default 分岐でコンパイラに never と言わせられれば、すべてのケースが処理されている静的な証明が得られます。これが 網羅性チェック の基礎です。
前のモジュールで判別可能ユニオンを知りました。今度はそれを本物の設計ツールに変える番です。省略可能なフィールドを詰め込んだ単一のインターフェースと比べて、判別可能ユニオンは 不可能な状態を表現不可能にします。data と error を同時に持つことも、データのない成功を持つこともできません。読み込み状態、操作結果、イベントメッセージにうってつけのパターンです。
type FetchState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function render(state: FetchState<string[]>): string {
switch (state.status) {
case "idle": return "Waiting";
case "loading": return "Loading…";
case "success": return state.data.join(", ");
case "error": return state.error.message;
}
}default 分岐で呼ばれる assertNever ガードにはすでに出会っています。ここでその仕組みを正確に分解しましょう。すべてのケースがカバーされている限り、その引数はそこで never 型になり、すべてがコンパイルされます。switch を更新せずにユニオンにバリアントを追加すると、忘れられたバリアントはもはや never に代入できなくなります。コンパイル時エラーとなり、メッセージには欠けているケースの名前が示されます。本番のバグを、拒否されるビルドへと変えるのです。
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rect"; width: number; height: number };
function assertNever(value: never): never {
throw new Error("Unhandled case: " + String(value));
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
case "rect": return shape.width * shape.height;
default: return assertNever(shape);
}
}注目に値する高度な細部が 2 つあります。第一に、判別子による絞り込みが入れ子の構造に伝播するのは、囲んでいるオブジェクトを直接テストした場合だけです。判別子だけを分割代入すると結び付きが切れます(TypeScript 4.6 以降で扱えるようになった共同分割代入のケースを除く)。第二に、never はユニオンで吸収されます(string | never は単に string です)。これが、Exclude のように条件型がバリアントを取り除ける理由です。
type Event =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string };
type EventOf<T extends Event["type"]> =
Extract<Event, { type: T }>;
type ClickEvent = EventOf<"click">;
function handle<T extends Event["type"]>(
type: T,
handler: (event: EventOf<T>) => void
): void {
/* register the handler */
}
handle("click", (e) => console.log(e.x, e.y));assertNever を含む switch を変更せずに Shape にバリアントを追加すると何が起きますか?string | never という型は何になりますか?neverstringunknownstring | undefined