✅
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