🎯

ts-patternを使うのをやめた話

2023/09/20に公開

最近、「TypeScriptプロジェクトにスキーマ駆動開発を持ち込み、より型安全な世界へ(パターンマッチングを持ち込む)」という記事を読んで、試しに利用してみました。しかし、最終的には導入を見送る決断をしました。この記事では、その理由と代わりの対策について説明します。

導入を見送った背景

number型のenumにおいて網羅性チェックが不十分

issueでも取り上げられていますが、number型のenumは任意のnumber型を代入できるという型安全性に関するtypescriptの仕様に由来して、number型のenumを分岐keyに利用する場合に網羅性チェックが上手くいきません。

例として、以下のようなnumber型のenumがある場合を考えます。

enum NumberEnum {
  ZERO = 0,
  ONE = 1,
}

通常のts-patternの使用方法で書くと、以下のようになりますが、exhaustive()を使った場合に型エラーが発生します。

const handleNumberEnum = (value: NumberEnum) => {
  return match(value)
    .with(NumberEnum.ZERO, () => 'zero')
    .with(NumberEnum.ONE, () => 'one')
    .exhaustive();
  // Type 'NonExhaustiveError<NumberEnum>' has no call signatures.
};

型エラーを回避するためには、以下のように書く必要があります。

const handleNumberEnum = (value: NumberEnum) => {
  return match(value)
    .with(0, () => 'zero')  // NumberEnum.ZERO
    .with(1, () => 'one')   // NumberEnum.ONE
    .exhaustive();
};

代替手段

TypeScriptの公式ドキュメントで紹介されているnever型を用いた網羅性チェックの手法を採用しました。
具体的には、以下のようにコードを書きました。

const ensureAllCasesHandled = (key: never): never => key;

const handleNumberEnum = (value: NumberEnum) => {
switch (value) {
  case NumberEnum.ZERO:
    return 'zero';
  case NumberEnum.ONE:
    return 'one';
  default:
    ensureAllCasesHandled(value);
}
};

この方法で、網羅性を確保しつつ、より型安全なコードを実現できました。

Discussion