satisfies で exhaustiveness check

2024/06/17に公開

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 違反なのでできない)
  • eslintno-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 いいね

Discussion