ReactのPropsの型定義を抽出し再定義を防ぐ

目次

こんにちは、@Himenonです。React の Props から特定の型定義を抽出する方法を様々なパターンで紹介します。

紹介する内容

この記事が活躍であろうケース

TypeScript で React のコンポーネントを作成していると、型の再定義を行うケースがあります。例えば、使用しているライブラリから型定義が export されておらず、自分で定義しなおしたり、アーキテクチャのルールで export できる型定義を制限していたり、自分で制御できないパターンなどがあります。ここで紹介する内容は、こういったケースの時、大元の型定義を参照したり抽出したりすることで、型の再定義を防ぐことができます。

※ ここでいう型の再定義は、元の型定義を参照することではなく、コピー&ペーストでそのまま持ってくることを指しています。

紹介する内容

  1. 参照型
  2. 配列から要素の型定義を抽出する
  3. Omit/Pick を利用して必要なプロパティだけ抽出する
  4. Exclude/Extract を利用して Union Type から特定の型定義を除外・抽出する
  5. 関数から型定義を抽出

これらは組み合わせて使うとより効果を発揮するような Tips となります。

1. 参照型

Props を以下のように定義したとき

export type Props = {
  text: string;
  numeric: number;
  bool: boolean;
  obj: {
    text: string;
    numeric: number;
    bool: boolean;
  };
  optionalValue?: string;
  nullValue: null;
  undefinedValue: undefined;
  arrayValue: number[];
  unionValue: string | number | boolean;
  symbolValue: Symbol;
};

value に当たる型定義を抽出すると次のようになります。

type Text = Props["text"]; // string
type Numeric = Props["numeric"]; // number
type Bool = Props["bool"]; // boolean
type Obj = Props["obj"]; // { text: string; numeric: number; bool: boolean; }
type OptionalValue = Props["optionalValue"]; // string | undefined
type NullValue = Props["nullValue"]; // null
type UndefinedValue = Props["undefinedValue"]; // undefined
type ArrayValue = Props["arrayValue"]; // number[]
type UnionValue = Props["unionValue"]; // string | number | boolean
type SymbolValue = Props["symbolValue"]; // Symbol

Playground

2. 配列から要素の型定義を抽出する

Playground

配列の要素の型定義を抽出する方法を紹介します。

export type Item = {
  name: string;
  age: number;
};

export type Props = {
  array: Item[];
  tupleArray: [Item, Item, Item];
  multiTypeTuple: [Item, string, number];
};

[0]の抽出

配列の index 値[0]を指定したときの結果はすべて Item 型になります。

type ExtractItem0 = Props["array"][0]; // Item
type TupleItem0 = Props["tupleArray"][0]; // Item
type MultiTypeTuple0 = Props["multiTypeTuple"][0]; // Item

[1], [2], [3]の抽出

配列の index を指定した場合、次のような結果になります。

type ExtractItem1 = Props["array"][1]; // Item
type ExtractItem2 = Props["array"][2]; // Item
type ExtractItem3 = Props["array"][3]; // Item

type Tuple1 = Props["tuple"][1]; // Item
type Tuple2 = Props["tuple"][2]; // Item
type Tuple3 = Props["tuple"][3]; // undefined

type MultiTypeTuple1 = Props["multiTypeTuple"][1]; // string
type MultiTypeTuple2 = Props["multiTypeTuple"][2]; // number
type MultiTypeTuple3 = Props["multiTypeTuple"][3]; // undefined

Props[“array”]で要素を指定した場合の型定義は常に Item を返します。Tuple の場合、宣言されていない要素にアクセスした場合はundefinedを返します。それと同時に以下のようなエラーが発生します。

Tuple type ‘[Item, string, number]’ of length ‘3’ has no element at index ‘3’.(2493)

[number]の抽出

number を指定した場合の挙動を見ると次のようになります。

type ArrayItem = Props["array"][number]; // Item
type TupleItem = Props["tuple"][number]; // Item
type MultiTypeTupleItem = Props["multiTypeTuple"][number]; // string | number | Item

number で配列の要素を抽出した場合は、Union Type で結果が出力されます。前述の ArrayItem と TupleItem は要素が常に Item を返すため、Item 型で算出されます。MultiTypeTupleItem は string | number | Item で結果が出力されます。

注意:[number]を指定した場合、 Props["tuple"][3] の場合の undefined は結果に含まれません。

3. Omit/Pick を利用して必要なプロパティだけ抽出する

コンポーネントを組み合わせていくうちに、最終的に露出する型定義が減っていくことがあります。露出するプロパティが多いと、コンポーネントを使用する際に重複したプロパティを渡してしまったり、どのプロパティを指定して良いかわからないケースがあります。Omit を利用して自由度を下げておくことで実装時に迷うことを減らすことができます。

import * as React from "react";

export type NavigationItemProps = {
  label: string;
  onClick: () => void;
};

const NavigationItem: React.FC<NavigationItemProps> = (props) => {
  return (
    <div>
      <button onClick={props.onClick}>{props.label}</button>
    </div>
  );
};

export type NavigationProps = {
  deleteItem: Omit<NavigationItemProps, "label">; // label以外を抽出
  addItem: Pick<NavigationItemProps, "onClick">; // onClickだけ抽出
};

const Navigation: React.FC<NavigationProps> = (props) => {
  const items: NavigationItemProps[] = [
    {
      ...props.deleteItem,
      label: "DELETE",
    },
    {
      ...props.addItem,
      label: "ADD",
    },
  ];

  return (
    <div>
      {items.map((item) => (
        <NavigationItem key={item.label} {...item} />
      ))}
    </div>
  );
};

Playground

4. Exclude/Extract を利用して Union Type から特定の型定義を除外・抽出する

4.1. Optional の除外

次のように定義された CustomButtonProps の onClick は optinoal と Callback の Union Type になっています。

type MyEvent = {};

type CustomButtonProps = {
  onClick?: (event: MyEvent) => void;
};

(event: MyEvent) => void だけ抽出するためには optional の部分を次のように除外します。

type ClickCallback = Exclude<CustomButtonProps["onClick"], undefined>;

4.2. Union Type の抽出

例えば、正方形、長方形、三角形を描画するコンポーネントの Props がDrawPropsとして次のように定義されているとします。

type SquareProps = {
  kind: "square";
  size: number;
};

type RectangleProps = {
  kind: "rect";
  width: number;
  height: number;
};

type TriangleProps = {
  kind: "triangle";
  size1: number;
  size2: number;
  size3: number;
};

type ShapeProps = SquareProps | RectangleProps | TriangleProps;

export type DrawProps = {
  shape: ShapeProps;
};

このコンポーネントを利用する際、DrawPropsしか型定義が提供されていない場合、取り扱いに少々困ります。Extract を利用すると、変更することなく、次のように抽出することができます。

type ExtractSquareProps = Extract<DrawProps["shape"], { kind: "square" }>;
type ExtractRectangleProps = Extract<DrawProps["shape"], { kind: "rect" }>;
type ExtractTriangleProps = Extract<DrawProps["shape"], { kind: "triangle" }>;

4.3. Exclude と Extract の解説

これは Extract は次のように定義されており、

type Extract<T, U> = T extends U ? T : never;

T が U の部分型の場合、T が返却されます。ここで紹介した

type ShapeProps = SquareProps | RectangleProps | TriangleProps;

は、kind プロパティによって判別可能な Union 型であるため、この部分型である型定義を ShapeProps から抽出することが可能です。

Utility 型の Exclude は Extract の condition を反転させたもので次のように定義されています。

type Exclude<T, U> = T extends U ? never : T;

紹介した Exclude<CustomButtonProps[“onClick”], undefined> は CustomButtonProps から undefined の部分型を除外する結果を返しています。

5. 関数から型定義を抽出

5.1. Parameter を利用して Callback の引数を取得する

例えば、HTML の button タグのonClickの第一引数を取得するような場合を考えます。React の型定義を使うと、button の Props は次のように定義できます。

import * as React from "react";

type ButtonProps = React.DetailedHTMLProps<
  React.ButtonHTMLAttributes<HTMLButtonElement>,
  HTMLButtonElement
>;

ここから、まずは onClick を抽出すると、optional を除外して次のようになります。

type ClickCallback = Exclude<ButtonProps["onClick"], undefined>;

ここから引数を抽出するには、 Utiity 型の Parameters を利用して、

type ButtonClickEvent = Parameters<ClickCallback>[0];

と定義することができます。Parameters は関数を引数として取る Utility 型で、各引数を配列型で返却します。

Playground

5.2. ReturnType を利用して Callback の返り値を取得する

前述の ClickCallback から返り値の型定義を抽出すると次のようになります。

type ClickCallbackReturnType = ReturnType<ClickCallback>; // void

ReturnType は関数の型定義を引数に取り、その返り値の型定義を返します。

Playground

終わりに

ここで紹介した内容は Utility 型を使った型参照・抽出の方法です。これらを用いれば大抵のケースでうまく型定義を実装内に当てはめることができるでしょう。

本記事ではほとんど触れませんでしたが、Utility 型は内部ではあまり馴染みのない計算が行われています。Utility 型を使った利用パターンを利用することで思考のショートカットができるのでぜひ使ってみてください。