本記事はAdvent Calendar 2021「タイムリープTypeScript 〜TypeScript始めたてのあの頃に知っておきたかったこと〜」9日目の記事です。
概要
個人的にTypeScriptを始めた頃に用途を知っておきたかった型No.1の never
について紹介します。
neverは値が存在しないことを表現することができる型です。
「値が無い型をいつ使うんだ?」と私も当初考えていましたが、値が存在しない型というのを逆に考えると「never型に値が代入されるコードを書くと型エラーになる」ということです。
この「代入されると型エラーになる」という仕組みを使うと実装漏れを型エラーで指摘してくれるようになります。特にこの恩恵はスキーマ駆動で型を自動生成しているときに感じることが多いです。
例
neverの使い方が一番わかりやすいのはEnum型もしくはUnion型の値を判別して処理を行う場合でしょう。
特定の状態を表すステータスと、それに対応する処理を行う関数があります。
type Status = "reviewing" | "approved" | "rejected";
const statusCheck = (status: Status) => {
switch (status) {
case "reviewing":
console.log("レビュー中の処理");
break;
case "approved":
console.log("承認済みの処理");
break;
case "rejected":
console.log("却下時の処理");
break;
default:
break;
}
};
この時点では never は使用されていません。
では、ここに新機能としてdraft状態にできる機能が追加されました。
type Status = "reviewing" | "approved" | "rejected" | "draft";
draft状態が追加されたことによって、ステータスが関連する処理を全て書き換える必要があります。
使用されている場所がこの1箇所だけなら忘れてしまうことはほぼないでしょうし、問題も起きないでしょう。しかしこのUnionを使用した処理がいくつもあり、その実装が数ヶ月前だったり別人が対応したものだったりした場合大変です。
ステータスに関連するコードを全検索して変更、動作確認・テストで全体的に問題がないことを確認して、それでももしかしたらまだ使っているところがあるのではないかという不安を抱えることになります。
この問題を解決するのが never です。
先ほどのステータスチェック関数に never を組み込んでみます。
type Status = "reviewing" | "approved" | "rejected";
const statusCheck = (status: Status) => {
switch (status) {
case "reviewing":
console.log("レビュー中の処理");
break;
case "approved":
console.log("承認済みの処理");
break;
case "rejected":
console.log("却下時の処理");
break;
default:
const unreachable: never = status;
break;
}
};
default節で never 型を使用しても、この状態では何の変化もありません。
では、先ほどと同じくdraft状態を追加してみます。
type Status = "reviewing" | "approved" | "rejected" | "draft";
...
case "rejected":
console.log("却下時の処理");
break;
default:
// 型 'string' を型 'never' に割り当てることはできません。
const unreachable: never = status;
break;
そうすると実装漏れがあることを型エラーによって知ることができるようになりました。
これは値を持たないはずの never 型に値を持っているstatus(string型)を代入しようとしているためです。
つまりdefault節に到達すると絶対にエラーが発生するため、それ以前に網羅チェックを行うことが強制されるのです!
この仕組みを使いやすくするために共通関数化しておきます。
const assertNever = (x: never) => {
throw new Error("This code should not be called");
};
const statusCheck = (status: Status) => {
switch (status) {
case "reviewing":
console.log("レビュー中の処理");
break;
case "approved":
console.log("承認済みの処理");
break;
case "rejected":
console.log("却下時の処理");
break;
case "draft":
console.log("draft時の処理");
break;
default:
assertNever(status);
break;
}
};
switchもしくはifで網羅チェックを行うときに、必ずこの関数を呼び出すようにしておけば機能追加や変更が入ったときに、すべての箇所の実装漏れを型エラーで検知することができるようになります。
(今回のコード例はswitchで書きましたが、網羅チェックでコードがそこまで到達しないことを表現できれば良いのでif文でも使用できます)
その他の使い方
neverは他にも「Discriminated Union」(Tagged Union)で使うことがあります。
同じプロパティを持ちつつも、特定のパターンが指定された時の型を絞り込むことができます。
TypeScriptの型システムの応用的な使い方なので本記事では解説しませんが、以下の記事を参考に実装してみてください。
参考