💭

TypeScript で値が Union Type にマッチするかを検証したい

3 min read

TypeScript version: v4.3.5

元々やりたかったのは以下のようなこと。外部入力の文字列を Union Type にマッチするか検証してマッチしなければデフォルト値を返すみたいなやつ。

const colors = ["red", "blue", "yellow"] as const;
type Color = typeof colors[number];
const defaultColor = colors[0];

function toColor(color: string): Color {
  return colors.includes(color) ? color : defaultColor; // Type 'string' is not assignable to type '"red" | "blue" | "yellow"'.
}

const color = toColor(localStorage.getItem("color") || "");

しかし上記のコードはエラー。気持ち的には colors.includes(color) が真なら colorColor と判定されてほしいけどそこまで賢くはなかった。(playground)

Type Guard 使ってこういう感じにしてみる。

const colors = ["red", "blue", "yellow"] as const;
type Color = typeof colors[number];
const defaultColor = colors[0];

function isColor(color: string): color is Color {
  return colors.includes(color); // Argument of type 'string' is not assignable to parameter of type '"red" | "blue" | "yellow"'
}

function toColor(color: string): Color {
  return isColor(color) ? color : defaultColor;
}

const color = toColor(localStorage.getItem("color") || "");

しかしこれもエラー。Array#includes には string でなく Color を渡さないといけないらしい。(playground)

渡すときに Color とわかってたら Array#includes で検証する必要ないですやん...。この問題については以下に書いてあった。

https://fettblog.eu/typescript-array-includes/

この記事では

function includes<T extends U, U>(coll: ReadonlyArray<T>, el: U): el is T {
  return coll.includes(el as T);
}

みたいなヘルパ関数を解決方法として挙げていたけど、これだったら includes にわたすときに as でキャストすればいいんじゃないかという気もする。こんな感じで(playground

function isColor(color: string): color is Color {
  return colors.includes(color as Color);
}

もしくは string にキャストした SetArrayでもいい)を作るとかでもいいかもしれない。(playground

const colorsSet = new Set<string>(colors);

function isColor(color: string): color is Color {
  return colorsSet.has(color);
}

他になにかいい方法あるんだろうか。