✅
satisfies で exhaustiveness check
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が改善されました。
そのため、複雑な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 いいね
Discussion