TypeScriptで重複を許さない固定値を型ガードで守りたい
よくあるような選択肢のSelectコンポーネント。
type SelectOption = {
value: number;
label: string;
};
const Select = () => {
const options: SelectOption[] = [
{ value: 1, label: 'りんご' },
{ value: 2, label: 'バナナ' },
{ value: 3, label: 'ブドウ' },
{ value: 4, label: 'もも' },
{ value: 5, label: 'ナシ' },
{ value: 6, label: 'いちご' }
];
return (
<select id='plan-select' name='plan-select'>
{options.map(option => {
return (
<option value={option.value} key={option.value}>
{option.label}
</option>
);
})}
</select>
);
};
値が被るリスクが怖い
しかしこの選択肢が、例えば選択でユーザーに請求する値段が変わるなど、クライアントのビジネスドメイン上非常に重要な意味を持つ場合、保守拡張の際に、うっかり重複した値を設定してしてしまい、バグを引き起こすのは非常にリスクが大きい。
ぱっと見間違えることは無さそうだが、こんな綺麗に1から順に並ばなくなることも保守上ではあり得る。
例えば「出る順番を並べ替えたい」などクライアントから言われたことで、うっかり
const options: SelectOption[] = [
{ value: 4, label: 'もも' },
{ value: 6, label: 'いちご' },
{ value: 5, label: 'ナシ' },
{ value: 2, label: 'バナナ' },
{ value: 1, label: 'りんご' },
{ value: 3, label: 'ブドウ' },
{ value: 4, label: '柿' }
// ⇧⇧⇧ すでにvalue:4が他に存在するのに誤って新しいデータのように挿入してしまった!
];
などとしてしまうかもしれない。
これだとユーザーは「柿」を入力したのにバックエンドでは「桃」と認識されてしまう。
もしユーザーが柿を買ったつもりで桃が届いたら大変なことだ。
しかも記述が間違っていても、納品前に選択肢を全て検証するなど非現実だし、テストもしにくい。
なるべく不確実でリスクのある要素は静的解析レベルで防ぎたい。
解決策
TypeScriptの静的解析で数独が作れるくらいである
今回の場合、オプション値は静的に管理しているため、静的解析が効かせられるのではないかと思った。
結果このような関数を作ることになった
(Claude先生にも相談しつつ)
type EnsureUniqueValues<T extends readonly { value: string | number }[]> = {
[K in keyof T]: T[K] extends { value: infer V } ? (Extract<T[number], { value: V }> extends T[K] ? T[K] : never) : never;
};
export function defineOptions<T extends readonly { value: string | number }[]>(options: T & EnsureUniqueValues<T>): T {
return options;
}
type Option = {
name: string;
value: number;
};
EnsureUniqueValuesでやってること
T extends readonly { value: string | number }[]:
Tは、valueプロパティを持つオブジェクトの読み取り専用配列
[K in keyof T]: ...:
Tの各要素(インデックスK)に対して処理を行うマップ型。
T[K] extends { value: infer V } ? ... : never:
T[K]がvalueプロパティを持つ場合、その型をVとして抽出。
Extract<T[number], { value: V }>:
配列T内で、valueがVである要素を全て抽出。
T[number]は、配列Tの要素全体を指すユニオン型。
Extract<T[number], { value: V }> extends T[K] ? T[K] : never:
valueがVである要素がT[K]自身のみであれば、T[K]をそのまま返す。
もし他にも同じvalueを持つ要素がある場合は、never型になる。
そしてOption値をas constアサーションでreadonlyのリテラル型にする。
またconstで複雑な型を扱う時は、satisfaisで型推論もやると吉。
出来たオプション値
const options: SelectOption[] = defineOptions([
{ id: 4, label: 'もも' },
{ id: 6, label: 'いちご' },
{ id: 5, label: 'ナシ' },
{ id: 2, label: 'バナナ' },
{ id: 1, label: 'りんご' },
{ id: 3, label: 'ブドウ' },
{ id: 4, label: '柿' },
] as const satisfies SelectOption[]);
静的解析の結果
このようにid的な絶対に被らせたくない値についても、TSの静的解析でチェックすることができる。
Discussion