この記事は Recruit Engineers Advent Calendar 2019 の 23日目の記事です。
TypeScript のプロジェクトで React コンポーネントを書いていると、コンパイラに怒られることがたびたびあります。ネット上にあるサンプルコンポーネントが JavaScript で書かれていると、プロジェクトにもってきたときにコンパイルできないということはよくあるんじゃないでしょうか。any でコンパイラを黙らせることもできますが、せっかく TypeScript を使っているので、安全に解決したいよねってことでこのような記事を書きました。
型定義は @types/react@16.9.17 にしたがっています。目次をみて、この程度のことはもう知っているよという方は react-typescript-cheatsheet をみてください。React + TypeScript のパターンがたくさんまとまっています。
基礎編
@types/react で用意されている型について説明します。なお、クラスコンポーネントについては説明しません。
関数コンポーネント (Function Components) の型
React.FC (React.FunctionComponent のショートハンド) という型が使えます。同じ型で React.SFC と React.StatelessComponent もありますが、こちらは現在非推奨です。
import React from "react";
type Props = {
  text: string;
}
const Test: React.FC<Props> = ({ children, text }) => (
  <div>
    <div>{text}</div>
    <div>{children}</div>
  </div>
);
props は React.FC<Props> のようにジェネリックで書くことができます。children は React.FC でコンポーネント定義すると暗黙的に使えるようになります。React.FC の型定義は以下のようになってます。
type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
    (props: PropsWithChildren<P>, context?: any): ReactElement | null;
    propTypes?: WeakValidationMap<P>;
    contextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}
type PropsWithChildren<P> = P & { children?: ReactNode };
関数コンポーネントの返り値は ReactElement または null となっています。
children の型
React.FC で定義される children はオプショナルであり、型は React.ReactNode です。props に children をあえて書くと以下のようになります。
type Props = {
  children?: React.ReactNode;
};
React.ReactNode は union 型で定義されています。
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
union されている型をさらに展開すると、
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
ReactChild は ReactElement、 string、 number の union 型です。ReactNode とあわせてみると、children として、string、number、boolean、null、および undefined のプリミティブ型が渡せることがわかります。  ReactFragment は ReactNode の配列型と定義されています。すると JSX のタグ構文で書くものは、ReactElement となります。説明を省略しましたが、ReactPortal は Portals のことです。
props の型
children 以外の props は自由に設計することになりますが、基本は TypeScript の理解だけで十分です。
type Props = {
  str: string;
  num: number;
  bool: boolean;
  obj: {
    str: string;
  };
  strArr: string[];
  objArr: {
    str: string;
  }[];
  func: () => void;
}
React で用意されている便利な型もあります。CSS プロパティやイベント関連の型は以下のようになります。
type Props = {
  // css プロパティ
  style?: React.CSSProperties;
  // click イベントオブジェクトを引数で受け取る
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
  // onClick と同じ、handler の型も用意されている
  onClick2?: React.MouseEventHandler<HTMLButtonElement>;
}
マウスイベントだけでなく、ドラッグやクリップボードなど、イベント関連は一通り型定義が用意されています。
初級編
プロジェクトを進めていて実際にやってみたことを抜粋して紹介します。
children の型を制限する
基礎編で説明したように children の型は明示的に定義することができるので、children の型を制限することもできます。以下に例を示します。
// children を受け付けない
type Props = {
  children?: never;
};
// 文字列のみ
type Props = {
  children?: string;
};
// 単一要素のみ
type Props = {
  children?: React.ReactChild;
};
// 単一要素 (`ReactElement`) のみ
type Props = {
  children?: React.ReactElement;
};
余談ですが、React.ReactChild とタイピングすると、React.ReactChildren という型もエディタの補完ででてきます。名前が紛らわしいですが React.ReactChildren は children を処理するためのユーティリティであり、children の型指定で使うものではありません。
HTML 要素の props を参照する
HTML 要素を少しだけ拡張したいというときなどに使えるテクニックです。HTML 要素の props は JSX.IntrinsicElements["hoge"] で参照できます。 以下のコンポーネントでは、 button 要素の props がすべて受け取り可能になります。
type MyButtonProps = {
  // ここで拡張できる
} & JSX.IntrinsicElements["button"];
// type は button の props
const MyButton: React.FC<MyButtonProps> = ({ children, type }) => (
  <button type={type}>{children}</button>
);
as props で render される要素を指定する
render される HTML 要素を動的に変えたいときに使います。styled-components などで使われているパターンです。
type Props = {
  as: React.ElementType<any>;
};
const Test: React.FC<Props> = ({ as: Component }) => (
  <Component />
);
const Parent = () => (
  <div>
    {/* a 要素になる */}
    <Test as="a" />
    {/* button 要素になる */}
    <Test as="button" />
  </div>
);
React.ElementType という型を使っています。この型を使うことで HTML の要素名を props に渡すことができます。上記では any を指定していますが、特定の要素を指定することもできます。以下では button 要素と a 要素のみ指定できるようにしています。
type Props = {
  as: React.ElementType<JSX.IntrinsicElements["button"] | JSX.IntrinsicElements["a"]>;
};
props を切り替える
受け付ける props のセットを切り替えることができます。<amp-img> のように layout 属性によって width / height 属性の有無が切り替わるときに使えるパターンです。<amp-img> は layout="fixed" のときに width と height、layout="fixed-height" のときに height のみを指定する必要があります。以下のように props を union 型で指定して、layout のガード節を入れることで props セットの切り替え再現することができます。
// width、height を定義
type FixedProps = { layout: "fixed"; width: number; height: number; };
// height のみ定義
type FixedHeightProps = { layout: "fixed-height"; height: number; };
const Image: React.FC<FixedProps | FixedHeightProps> = (props) => {
  if (props.layout ===  "fixed") {
    return <div>{props.layout}</div>;
  } else {
    // props.width // error
    return <div>{props.layout}</div>;
  }
}
const Parent = () => (
  <div>
    <Image layout="fixed" width={100} height={100} />
    <Image layout="fixed-height" height={100} />
    {/* <Image layout="fixed-height" width={100} height={100} /> // invalid */}
  </div>
);
さいごに
基礎編で Event や Form や Hooks についても書きたかったけど、アドベントカレンダーに間に合わなかった。型安全なコンポーネント作りは、極めていくと無限にやりたい事がでてきますが、基礎編程度をおさえておけば自分で調べて解決できると思います。はじめに紹介しましたが、react-typescript-cheatsheet に多くのパターンが紹介されているので、困ったときにぜひ参考にしてみてください。