TypeScriptでSwitch文で分岐するpropertyに新しいtypeが追加されたときにエラーになってほしい
switch
文使っていますか?
switch
文、昔はなんか読みにくいから避けろみたいに言われていたこともあった気がするんですが、Redux
が流行るようになって見直されて使われるようになったみたいな文脈があった気がしています。
確か当時、Redux
はswitch
を使っているから嫌だ、みたいな話さえ合った気がしていますが、今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だった';
}
};
この状態でPropertyType
にpiyo
を足すと、エラーになってくれます🙆🏻♂️
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.json
のcompilerOptions
でstrictNullChecks
がtrue
になっていない場合はエラーになりません。
もしこれが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}`);
}
}
};
といってエラーにしてくれます🙆🏻♂️便利ですね💪
この時、適当な関数を用意しておいて使い回すとよさそうです。
ここにそんなことが載っています
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
switch 文の網羅性については、自前で default case で
never
チェックをするパターンもありますが、 (型定義に緩いstring
ではなく string literal type union などを用いている前提で)@typescript-eslint/switch-exhaustiveness-check を使うと解決できる場合が多いと思います。参考までに。 https://zenn.dev/noshiro_piko/articles/take-full-advantage-of-typescript-eslint#switch-文
返信が遅くなってしまいすみません🙏
確かにlintで縛ってしまえるのであれば縛ってしまうのも手ですね🙆♂️
記事もありがとうございます!参考にさせていただきます🙌