🦔
渡した配列の要素の型で別の引数の型を制限する方法
やりたいこと
先日仕事で渡した配列の要素だけvalueとして指定できるようなコンポーネントを作成したいことがあったのですが実装方法がわからずに作ることができませんでした。 下記のような挙動をするコンポーネントです。今回このコンポーネントを作成したのでここにメモとして残しておきます。
// valid
const Example = () => (
<SelectBox
value={"a"}
items={["a", "b", "c"]}
onChange={(v) => console.log(v)}
/>
);
// dはaでもbでもcでもないのでerror
const ErrorExample = () => (
<SelectBox
value={"d"}
items={["a", "b", "c"]}
onChange={(v) => console.log(v)}
/>
);
型引数1つで実装
下記のように実装すると一見うまく動くように見えるのですが、ErrorExampleで型エラーが出ません。itemsだけでなく、valueからも型推論が行われて型が拡張されてしまうためです。
ErrorExampleではTは 'a' | 'b' | 'c' | 'd'
と推論されてしまいます。
type Props<T extends string> = {
value: T;
items: T[];
onChange: (value: T) => void;
};
export const SelectBox = <T extends string>({
value,
items = [],
onChange,
}: Props<T>) => (
<select onChange={(e) => onChange(e.target.value as T)}>
{items.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
);
// valid
const Example = () => {
return (
<SelectBox
value={"a"}
items={["a", "b", "c"]}
onChange={(v) => console.log(v)}
/>
);
};
// valid
const ErrorExample = () => {
return (
<SelectBox
value={"d"}
items={["a", "b", "c"]}
onChange={(v) => console.log(v)}
/>
);
};
型引数2つで実装
推論に使いたい型と、推論した型をもとに型のチェックを行う型の2つに分けて実装したところ、ErrorExampleのvalueでエラーが出て期待どおりの挙動になりました。先頭の型引数Tの型が型推論で決定すると自動的にUも決定されて、valueの型によってUの型が決まることがなくなるため型引数が1つのときと違ってvalueをエラーにしてくれるのだと思います。
type Props<T extends string, U extends T> = {
value: U;
items: T[];
onChange: (value: T) => void;
};
export const SelectBox = <T extends string, U extends T>({value, items = [], onChange,}: Props<T, U>) => (
<select onChange={(e) => onChange(e.target.value as T)}>
{items.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
);
// valid
const Example = () => {
return (
<SelectBox
value={"a"}
items={["a", "b", "c"]}
onChange={(v) => console.log(v)}
/>
);
};
// error
const ErrorExample = () => {
return (
<SelectBox
// TS2322: Type '"d"' is not assignable to type '"a" | "b" | "c"'.
value={"d"}
items={["a", "b", "c"]}
onChange={(v) => console.log(v)}
/>
);
};
Discussion