TL;DR
type A = 'a' | 'b' | 'c';
function exhaustive(v: A) {
switch (v) {
case 'a':
return 'A';
case 'b':
return 'B';
case 'c':
return 'C';
default:
return v satisfies never; // check exhaustiveness
}
}
はじめに
TypeScriptにsatisfies文が追加されて久しいですね。
satisfiesが導入される前は、switch 文の exhaustiveness (網羅性) チェックを行うために、以下のような実装をよくしていました。
type A = 'a' | 'b' | 'c';
function exhaustive(v: A) {
switch (v) {
case 'a':
return 'A';
case 'b':
return 'B';
case 'c':
return 'C';
}
const _: never = v;
}
または if 文を使って:
type A = 'a' | 'b' | 'c';
function exhaustive(v: A) {
if (v === 'a') { return 'A'; }
if (v === 'b') { return 'B'; }
if (v === 'c') { return 'C'; }
const _: never = v;
}
これにより、もし switch 文や if 文の条件分岐が変数 v に対して網羅的でない場合、never 型に v を代入することで、コンパイルエラーを発生させることができます。
しかし、いくつか問題がありました。
switch文の外でconst _: never = v;を書くのはなんとなく気持ち悪い(caseの中で値を宣言するのはno-case-declarations違反なのでできない)eslintのno-unused-varsなどのルールに引っかかる (一応_を除外する設定をすることもできるが)
satisfies で exhaustiveness check
TypeScript 4.9 から、satisfies が導入されました。
これを使うと、以下のように書くことができます。
type A = 'a' | 'b' | 'c';
function exhaustive(v: A) {
switch (v) {
case 'a':
return 'A';
case 'b':
return 'B';
case 'c':
return 'C';
default:
return v satisfies never; // check exhaustiveness
}
}
こちらの方がすっきりとしていますね。
また、eslint のルールにも引っかからないので、煩わしいエラーともおさらばできます。
switch(true) との組み合わせ
TypeScript 5.3 より、switch(true) による型のnarrowingが改善されました。
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-3.html#switch-true-narrowing
そのため、複雑なunion型に対しても、switch(true) と satisfies を組み合わせることで、網羅性チェックを行うことができます。
type A = [] | 3 | string;
function exhaustive(v: A) {
switch (true) {
case Array.isArray(v):
return '[]';
case v === 3:
return '3';
case typeof v === 'string':
return v;
default:
return v satisfies never; // check exhaustiveness
}
}
おわりに
satisfies いいね