🐮

TypeScriptでSwitch文で分岐するpropertyに新しいtypeが追加されたときにエラーになってほしい

4 min read

switch文使っていますか?
switch文、昔はなんか読みにくいから避けろみたいに言われていたこともあった気がするんですが、Reduxが流行るようになって見直されて使われるようになったみたいな文脈があった気がしています。
確か当時、Reduxswitchを使っているから嫌だ、みたいな話さえ合った気がしていますが、今switchに対する気持ちがそこまでネガティブではない気がするのは、Reduxによるところが大きいのかな?と思ったりしています。

Union型で同じプロパティを比較して返り値を一意に決めたいみたいなとき、switch文をよく使います。
例えば以下のような例ですね。

type valueOf<T> = T[keyof T];

const PROPERTY_TYPE = {
  hoge: 'hoge',
  fuga: 'fuga',
} as const;

type PropertyType = valueOf<typeof PROPERTY_TYPE>; // 'hoge' | 'fuga'

このProeprtyTypeに応じて、何かしら値を返したい、みたいな場合です。
僕は値だけでなく、Reactのコンポーネントを返したりなんかもよくやります。

そしてこのPropertyTypeに何かしら値が追加された場合は型でエラーになってほしい、みたいなのが今回の記事の内容です。
例えばバックエンドとopenapiか何かで型を共有している場合、変更に合わせて修正が必要な箇所は型エラーでビルドが落ちてくれると安心ですし、影響範囲がわかりやすいですよね。そういうモチベーションです🙆🏻♂️‍

返り値にundefinedが含まれない場合

例えばReact.FCなんかがそうですが、返り値の型にundefinedが含まれない場合は単純にdefault節を書かないという方法があります。

const switchFn = (propertyType: PropertyType): string => {
  switch (propertyType) {
    case PROPERTY_TYPE.hoge:
      return 'propertyTypeはhogeだった';
    case PROPERTY_TYPE.fuga:
      return 'propertyTypeはfugaだった';
  }
};

この状態でPropertyTypepiyoを足すと、エラーになってくれます🙆🏻♂️‍

const PROPERTY_TYPE = {
  hoge: 'hoge',
  fuga: 'fuga',
  piyo: 'piyo', // 追加
} as const;

// Error: 関数に終了の return ステートメントがないため、戻り値の型には 'undefined' が含まれません。ts(2366)
const switchFn = (propertyType: PropertyType): string => {
  switch (propertyType) {
    case PROPERTY_TYPE.hoge:
      return 'propertyTypeはhogeだった';
    case PROPERTY_TYPE.fuga:
      return 'propertyTypeはfugaだった';
  }
};

ただ、これはtsconfig.jsoncompilerOptionsstrictNullCheckstrueになっていない場合はエラーになりません。

https://www.typescriptlang.org/tsconfig#strictNullChecks

もしこれがtrueにできない場合は、他の方法を考える必要があります。

default節でneverを使う

neverはこういうところで活躍するわけですね。
neverには値が入ることは無いので、その挙動を利用します。
こんな感じ🙆🏻♂️‍

const switchFn = (propertyType: PropertyType): string => {
  switch (propertyType) {
    case PROPERTY_TYPE.hoge:
      return 'propertyTypeはhogeだった';
    case PROPERTY_TYPE.fuga:
      return 'propertyTypeはfugaだった';
    default: {
      const _propertyType: never = propertyType;
      throw new Error(`ここには来ないはずだぞ!! ${_propertyType}`);
    }
  }
};

このようにすると、

const PROPERTY_TYPE = {
  hoge: 'hoge',
  fuga: 'fuga',
  piyo: 'piyo', // 追加
} as const;

const switchFn = (propertyType: PropertyType): string => {
  switch (propertyType) {
    case PROPERTY_TYPE.hoge:
      return 'propertyTypeはhogeだった';
    case PROPERTY_TYPE.fuga:
      return 'propertyTypeはfugaだった';
    default: {
      // 型 'string' を型 'never' に割り当てることはできません。ts(2322)
      const _propertyType: never = propertyType;
      throw new Error(`ここには来ないはずだぞ!! ${_propertyType}`);
    }
  }
};

といってエラーにしてくれます🙆🏻♂️‍便利ですね💪
この時、適当な関数を用意しておいて使い回すとよさそうです。
ここにそんなことが載っています

https://stackoverflow.com/a/39419171
const assertUnreachable = (x: never): never => {
  throw new Error(`Unexpected value!! ${x}`);
}

const switchFn = (propertyType: PropertyType): string => {
  switch (propertyType) {
    case PROPERTY_TYPE.hoge:
      return 'propertyTypeはhogeだった';
    case PROPERTY_TYPE.fuga:
      return 'propertyTypeはfugaだった';
    default:
      // 型 'string' の引数を型 'never' のパラメーターに割り当てることはできません。ts(2345)
      return assertUnreachable(propertyType)
  }
};

バッチリですね💪
もちろんちゃんとpiyoをハンドリングしてやるとエラーは消えます。

const switchFn = (propertyType: PropertyType): string => {
  switch (propertyType) {
    case PROPERTY_TYPE.hoge:
      return 'propertyTypeはhogeだった';
    case PROPERTY_TYPE.fuga:
      return 'propertyTypeはfugaだった';
    case PROPERTY_TYPE.piyo:
      return 'propertyTypeはpiyoだった';
    default:
      // エラーにならない
      return assertUnreachable(propertyType)
  }
};

便利ですね

型エラーをうまく使って変更の影響範囲のわかりやすいコードにしたいですね🙆🏻♂️‍

Discussion

ログインするとコメントできます