O hirunewani blog

Reactのpropsをunion typeにする方法について

Created at

Reactのpropsをunion typeにする方法をいくつか紹介する。ただし、これらの利用やそもそもunion typeをpropsに使うこと自体が適切かどうかは慎重に検討する必要がある。

Union types of props

複雑なサービスを作っていると、Union typesを1つのメソッドで処理したくなることがあるかもしれない。しかし安直に書いてしまうと上手くいかない。このような型定義にライブラリで出会ってしまうと最悪な気持ちになる。

export type Props =
  | {
      text: Item;
    }
  | {
      multilineText: Item;
    }
  | {
      select: Item;
    };

export const SomeComponent = (props: Props) => {
  return (
    <div>
      {props?.text ? (
        <div>{props?.text.value}</div>
      ) : props?.multilineText ? (
        <div>{props?.multilineText.value}</div>
      ) : props?.select ? (
        <div>{props?.select.value}</div>
      ) : null}
    </div>
  );
};

Kind pattern

各型に共通のプロパティを持たせ、それぞれの値を固有のLiteral typeなどにするパターン。

このプロパティを比較することで、どの型を利用するかの識別が可能になる。

export type Props =
  | {
      kind: "text";
      text: Item;
    }
  | {
      kind: "multilineText";
      multilineText: Item;
    }
  | {
      kind: "select";
      select: Item;
    };

const SomeComponent = (props: Props) => {
  return (
    <div>
      {props?.kind === "text" ? (
        <div>{props?.text.value}</div>
      ) : props?.kind === "multilineText" ? (
        <div>{props?.multilineText.value}</div>
      ) : props?.kind === "select" ? (
        <div>{props?.select.value}</div>
      ) : null}
    </div>
  );
};

Optional never Trick

次のように、擬似的に全ての型へ同様のプロパティを持たせることで対応できる。

このパターンの場合、レスポンスなどで受け取ったオブジェクトを編集しなくていい場合があるというメリットと、プロパティを列挙しなければならないというデメリットがある。

export type Props =
  | {
      text: Item;
      multilineText?: never;
      select?: never;
    }
  | {
      text?: never;
      multilineText: Item;
      select?: never;
    }
  | {
      text?: never;
      multilineText?: never;
      select: Item;
    }
  | {
      text?: never;
      multilineText?: never;
      select?: never;
    };

const SomeComponent = (props: Props) => {
  return (
    <div>
      {props?.text ? (
        <div>{props?.text.value}</div>
      ) : props?.multilineText ? (
        <div>{props?.multilineText.value}</div>
      ) : props?.select ? (
        <div>{props?.select.value}</div>
      ) : null}
    </div>
  );
};

次のようなUtility typeを生やしておけば、デメリットを解消できる。

type XOR<
  T extends Record<string, unknown>,
  U extends string = T extends T ? keyof T : never,
> = T extends T
  ? {
      [k in keyof T]: T[k];
    } & {
      [k in Exclude<U, keyof T>]?: never;
    }
  : never;

export type Props = XOR<
  | {
      text: Item;
    }
  | {
      multilineText: Item;
    }
  | {
      select: Item;
    }
>;

export const SomeComponent = (props: Props) => {
  return (
    <div>
      {props?.text ? (
        <div>{props?.text.value}</div>
      ) : props?.multilineText ? (
        <div>{props?.multilineText.value}</div>
      ) : props?.select ? (
        <div>{props?.select.value}</div>
      ) : null}
    </div>
  );
};

Utility types of TypeScript

https://www.typescriptlang.org/docs/handbook/utility-types.html

TypeScriptには便利なユーティリティタイプがいくつか同梱されている。

個人的に最低限知っておいた方がいいと思うものを列挙する。

  • Required:Objectの全てプロパティを必須にできる
  • Partial:Objectの全てのプロパティをオプショナルにできる
    
    const Page = (props: PageProps) => {
       return ...
    }
    
    type Params = Partial<PageProps>
    
    const PageContainer = ( ) => {
       const params = useParams<Params>()
       return params?.index === undefined ? null : <Page {...params} />
    }
    • Parameters:関数の引数を取得する
    • ReturnType:関数の戻り値を取得する
    const requestGetSomething = (args: Parameters<typeof endpoints.getSomething>) => {
        return fetcher(endpoints.getSomething(args), {...})
    }
    • Pick<Object, Keys>:オブジェクトから特定のプロパティのみを取り出す
    • Omit<Object, Keys>:オブジェクトから特定のプロパティを取り除く
    interface TodoProps {
      onEdit?: () => void;
    }
    
    interface ReadonlyTodoProps extends Omit<TodoProps, "onEdit"> {}