TypeScript で書く React コンポーネントを基礎から理解する

この記事は [Recruit Engineers Advent Calendar 2019](https://adventar.org/calendars/4502) の 23日目の記事です。

TypeScript のプロジェクトで React コンポーネントを書いていると、コンパイラに怒られることがたびたびあります。ネット上にあるサンプルコンポーネントが JavaScript で書かれていると、プロジェクトにもってきたときにコンパイルできないということはよくあるんじゃないでしょうか。`any` でコンパイラを黙らせることもできますが、せっかく TypeScript を使っているので、安全に解決したいよねってことでこのような記事を書きました。

型定義は `@types/react@16.9.17` にしたがっています。目次をみて、この程度のことはもう知っているよという方は [react-typescript-cheatsheet](https://github.com/typescript-cheatsheets/react-typescript-cheatsheet) をみてください。React + TypeScript のパターンがたくさんまとまっています。

## 基礎編

`@types/react` で用意されている型について説明します。なお、クラスコンポーネントについては説明しません。

### 関数コンポーネント (Function Components) の型
`React.FC` (`React.FunctionComponent` のショートハンド) という型が使えます。同じ型で `React.SFC` と `React.StatelessComponent` もありますが、こちらは現在非推奨です。

```tsx
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` の型定義は以下のようになってます。

```tsx
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 をあえて書くと以下のようになります。

```typescript
type Props = {
  children?: React.ReactNode;
};
```

`React.ReactNode` は union 型で定義されています。

```tsx
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
```

union されている型をさらに展開すると、

```tsx
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](https://reactjs.org/docs/portals.html) のことです。
 
### props の型

children 以外の props は自由に設計することになりますが、基本は TypeScript の理解だけで十分です。

```tsx
type Props = {
  str: string;
  num: number;
  bool: boolean;
  obj: {
    str: string;
  };
  strArr: string[];
  objArr: {
    str: string;
  }[];
  func: () => void;
}
```

React で用意されている便利な型もあります。CSS プロパティやイベント関連の型は以下のようになります。

```typescript
type Props = {
  // css プロパティ
  style?: React.CSSProperties;
  // click イベントオブジェクトを引数で受け取る
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
  // onClick と同じ、handler の型も用意されている
  onClick2?: React.MouseEventHandler<HTMLButtonElement>;
}
```
マウスイベントだけでなく、ドラッグやクリップボードなど、イベント関連は一通り型定義が用意されています。

## 初級編

プロジェクトを進めていて実際にやってみたことを抜粋して紹介します。

### children の型を制限する

基礎編で説明したように children の型は明示的に定義することができるので、children の型を制限することもできます。以下に例を示します。

```tsx
// 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 を処理するためのユーティリティ](https://ja.reactjs.org/docs/react-api.html#reactchildren)であり、children の型指定で使うものではありません。

### HTML 要素の props を参照する

HTML 要素を少しだけ拡張したいというときなどに使えるテクニックです。HTML 要素の props は `JSX.IntrinsicElements["hoge"]` で参照できます。 以下のコンポーネントでは、 button 要素の props がすべて受け取り可能になります。

```tsx
type MyButtonProps = {
  // ここで拡張できる
} & JSX.IntrinsicElements["button"];

// type は button の props
const MyButton: React.FC<MyButtonProps> = ({ children, type }) => (
  <button type={type}>{children}</button>
);
```

[TypeScript Playground](https://www.typescriptlang.org/play/?jsx=2&ssl=20&ssc=3&pln=3&pc=1#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wFgAoCmATzCTgFlqAhAVxhggDsAFHMAM5wAvHADeFOHAD00uIGUGeYHMGQIdGgVn0lgWQZA0QwUAvnABkcAFIBlABoA6AJJcYUYFwHA0AUQA2SEEnsCA2gBEAEbsnFyBALoA3BQUsnA0dHCA9gxwoRzccIB2DHBg-AIUaNwC8ExsmVwAXIjEMFYAYgDCADzlYdx8EIIAfCJwABRicGgAFsAeACZEXAA0ibT0egCUIn0DknAtGeELdMJiSUh6PWJjE9O+ei3SO9w9FMvRQA)

### as props で render される要素を指定する

render される HTML 要素を動的に変えたいときに使います。styled-components などで使われているパターンです。

```tsx
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>
);
```

[TypeScript Playground](https://www.typescriptlang.org/play/?jsx=2&ssl=21&ssc=3&pln=3&pc=1#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wFgAoCmATzCTgAUcwBnOAXjgG8K44UWALkTEYAOgCiAGyQgkAOxgAVWkgA8KedQB8AbgoBffZXJoI8lvCVJLw5OnEAxAMJqmEVto5wAFF35CcM64kPIK8AYAlBxePrxwasHg5uFwAPTaFJHGFGYW8AwoRIrePtHssfFqACbAAG6Z5Hx8XGkAVPxwgIGRgAS+gNYMgFYMgNEMcG1pBvF8ataWAewARCjz6Y3N3O1wAEYArjAw5t39w6Pjkwkz8AILO3vmyxlVabUNWbpAA)

`React.ElementType` という型を使っています。この型を使うことで HTML の要素名を props に渡すことができます。上記では `any` を指定していますが、特定の要素を指定することもできます。以下では button 要素と a 要素のみ指定できるようにしています。

```tsx
type Props = {
  as: React.ElementType<JSX.IntrinsicElements["button"] | JSX.IntrinsicElements["a"]>;
};
```
[TypeScript Playground](https://www.typescriptlang.org/play/?jsx=2&ssl=4&ssc=85&pln=4&pc=56#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wFgAoCmATzCTgAUcwBnOAXjgG8K44UWALkTEYAOgCiAGyQgkAOxgAVWkgA8AKQDKADTEBJRVGDyWwNNNkKYLANoAiAEYBXGDAjz7AXTgAfONp6hjDGpuaWcop29ijeAHwA3BQAvkmU5GgeLPBKSNnCyOjiAGIAwmpMEKxxHHAAFFz8QnCluJDy1nDJAJQcNXW8cGqt4B6dAPRxFN1pFJmm8AwoRIq1db3s-YNqACbAAG5T5Hx8XOMAVPxwgIGRgAS+gNYMgFYMgNEMcOfjyYN8arnZTewxexwSZfbgXOAuNweG4PF5vD6gn55eACAGQ9yeYFHE5gy5IKA4KDwz7HE5Iv6o+x7fZAkGktTjalHGZAA)

### props を切り替える

受け付ける props のセットを切り替えることができます。`<amp-img>` のように layout 属性によって `width / height` 属性の有無が切り替わるときに使えるパターンです。`<amp-img>` は `layout="fixed"` のときに `width` と `height`、`layout="fixed-height"` のときに `height` のみを指定する必要があります。以下のように props を union 型で指定して、layout のガード節を入れることで props セットの切り替え再現することができます。

```tsx
// 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>
);
```
[TypeScript Playground](https://www.typescriptlang.org/play/?jsx=2#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wFgAoCmATzCTgDFgAPJAEwAUcwBnOAXjgBvOABsU1CAFcYALjgAiTC3YKA3HADuwNjAAW8gHZSQAIyRQNepMADmeuXGNmLGgL5qqtek1ZsAEjb2MFwQvALCYhLSjkoqbAC01nYO6nDJwUYm5pZwHhQUaBCGPPAAkiAotkjyyOgwAHQMAMIAPL7soeEAPozxgSkh3DwAfBEAFGDDAJQCY0IUcHDAmHCTww3ikjIC-IKKyn5JQamzC+RLS0QwUlCGcK1swABuI0JTYTyb0TJurQD0T1engueTgSFEPHo50ucH+-zgH14DW0uj0cIRFhwUEWl2ut3ujxebyRXy2MT+gOJIKWbgodMo5CKJXgHBQREM8EE41m-DG41xRNeuKWrQqVXo5Jk-DifgUWh0+n4QgAjAAGNVudInGDK9WauEjEUPcXVKLbXWy9jHQbyjIOPUarX-I2gpZCf4AKhNlTNUsth2t9pg8tRStVTu1g0dBpdGOWhmeKFEOjgnv+DNFVOF5GmaiAA)

## さいごに

基礎編で Event や Form や Hooks についても書きたかったけど、アドベントカレンダーに間に合わなかった。型安全なコンポーネント作りは、極めていくと無限にやりたい事がでてきますが、基礎編程度をおさえておけば自分で調べて解決できると思います。はじめに紹介しましたが、[react-typescript-cheatsheet](https://github.com/typescript-cheatsheets/react-typescript-cheatsheet) に多くのパターンが紹介されているので、困ったときにぜひ参考にしてみてください。