🦔

渡した配列の要素の型で別の引数の型を制限する方法

2021/10/17に公開

やりたいこと

先日仕事で渡した配列の要素だけ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