🧐

TypeScriptのexhaustiveness checkをスマートに書く

に公開8

Discussion

YAMAMOTO YujiYAMAMOTO Yuji

それどこか、逆に分岐漏れがあるときにのみ型エラーが起きてしまう。

「それどころか、逆に分岐漏れがないときにのみ型エラーが起きてしまう。」ですかね。説明しているコードでは action.typenever になるでしょうし。

Ryo Ota - @nwtgckRyo Ota - @nwtgck

偶然 type: "__invalid__" が登場してしまう可能性への対応を考えてみました。

throw new Error(`Unknown type: ${(action as { readonly type: unique symbol} as { type: unknown }).type}`);

追記

シンプルに以下で良さそうな気がしてきました。

throw new Error(`Unknown type: ${(action as { type: never }).type}`);

Playground

lumaluma

追記の (action as { type: never }).type は実はすべての構造体にタグ以外の余分なプロパティがある必要があって,

type HeadAction = {
  type: "Head";
};

が候補にあったりすると,すり抜けてしまいます

Playground

Hideaki NoshiroHideaki Noshiro

https://typescript-eslint.io/rules/switch-exhaustiveness-check/

この eslint ルールを有効にすれば default ケースを書かない前提で網羅性チェックが可能だと思うのですが、上の eslint ルールを使用しなくて済むことを重視した手法、あるいは他のメリットがあるという話だったりするのでしょうか?

lumaluma

冒頭に

分岐が網羅的であることの保証を実行時と型検査時の両方で賢く行う方法

とあるように,実行時にも検査したい,という前提があり, switch-exhaustiveness-check では実行時の検査はできないという点が異なるかと思います

メリットで言えば,私の意見ですが,

  • 型が付いてるものの,ユーザーインプットだったり特殊な状況で関係ないものが来る,もしくは関連サービスがアップデートで新たな構造体が入ってくるような変更をして,型で想定しないものが実行時に到達しうる(新しいイベントタイプができました,とか)
  • HMR等で型チェック/ESLintが通ってない状態でもトランスパイルしてとりあえず動かすときに到達しうる

みたいなときに,早めに失敗してくれるのが単純に嬉しい (fail fast) と思っています

lumaluma

TypeScriptのsatisfies使ったら "__invalid__" のような決め打ちなしで行けないかなと試してみたら,以下のようなものができました(なにかしら @typescript-eslint/restrict-template-expressions に引っかからないものを 0 の位置に置く必要はありますが,何をおいても,それが通り抜けていても検知できます)

(action satisfies never as { type: 0 }).type
geDemgeDem

初心者質問で恐縮ですが、こちらの方法は
const exhaustiveCheck:never = action
を記述する方法と比べてどういった点で優れているのでしょうか