Help us understand the problem. What is going on with this article?

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

この記事は 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.SFCReact.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;

ReactChildReactElementstringnumber の union 型です。ReactNode とあわせてみると、children として、stringnumberbooleannull、および undefined のプリミティブ型が渡せることがわかります。 ReactFragmentReactNode の配列型と定義されています。すると JSX のタグ構文で書くものは、ReactElement となります。説明を省略しましたが、ReactPortalPortals のことです。

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.ReactChildrenchildren を処理するためのユーティリティであり、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>
);

TypeScript Playground

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

TypeScript Playground

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

type Props = {
  as: React.ElementType<JSX.IntrinsicElements["button"] | JSX.IntrinsicElements["a"]>;
};

TypeScript Playground

props を切り替える

受け付ける props のセットを切り替えることができます。<amp-img> のように layout 属性によって width / height 属性の有無が切り替わるときに使えるパターンです。<amp-img>layout="fixed" のときに widthheightlayout="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>
);

TypeScript Playground

さいごに

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away