React + TypeScript: ReactでTypeScriptを使うとき基本として知っておきたいこと

ReactでTypeScriptを使おうとすると、TypeScriptの基礎知識だけでは足りず、Reactの型について知らなければなりません。それで、戸惑うことも多いでしょう。そういう人たちのためにReactとTypeScriptの使い方をまとめたのが「[React+TypeScript Cheatsheets](https://github.com/typescript-cheatsheets/react#reacttypescript-cheatsheets)」です。

「React+TypeScript Cheatsheets」の情報は多いので、本稿は基本的な項目と、説明を補わないとわかりにくい点について、かいつまんでご紹介します。関数とフックを中心にして、クラスについては省きました。


# 関数コンポーネント
まずは、[関数コンポーネント](https://github.com/typescript-cheatsheets/react#function-components)です。普通の関数と同じく、引数を型づけしてください。JSXの要素を返せば戻り値は推論されます。

```react
type AppProps = { message: string }; // interfaceでもよい
const App = ({ message }: AppProps) => <div>{message}</div>;
```

戻り値を型づけする場合には、`JSX.Element`です。正しい値が返されなければ、エラーが示されます。

```react
const App = ({ message }: AppProps): JSX.Element => <div>{message}</div>;
```

型エイリアスやインタフェースは定めずに、型注釈を直接書き加えても構いません。型をほかで使わないときは、この方が簡単です。

```react
const App = ({ message }: { message: string }) => <div>{message}</div>;
```

引数と戻り値をそれぞれ定めるのでなく、関数そのものに型づけすることもできます。

ただし、コンポーネントの関数を`React.FunctionComponent`または`React.FC`で型づけするのは、~~これからは~~React 17まではお勧めできません。これらの型が[TypeScriptの基本テンプレートから除かれる](https://github.com/facebook/create-react-app/pull/8177)からです。

```react
const App: React.FunctionComponent<{ message: string }> = ({ message }) => (
	<div>{message}</div>
);
```

Reactの関数コンポーネントであることをはっきりと示したいなら、React 17までは[`React.VoidFunctionComponent`](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/46643)(`React.VFC`)、React 18からは`React.FunctionComponent`(`React.FC`)を使うのがよいでしょう。ただし、プロパティに`children`を受け取るときは、型定義に含めなければなりません。その場合の型は[`React.ReactNode`](https://github.com/typescript-cheatsheets/react#useful-react-prop-type-examples)です。

```react
const App: React.VoidFunctionComponent<{ message: string, children: React.ReactNode }> = ({ message, children }) => (
	<div>
		{message}
		{children}
	</div>
);
```

React 18では`React.FunctionComponent`が、デフォルトでは`children`を暗黙で受け取らないように改められ~~るそうです~~ました。そのため、~~将来的には~~React 18からは`VoidFunctionComponent`は推奨され~~なくなります~~ません。

React 18について補っておきましょう。型定義[`DefinitelyTyped/types/react/index.d.ts`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L517-L541)によれば、つぎのように`React.FunctionComponent`の定めは`React.VoidFunctionComponent`と同じになりました。つまり、`React.VoidFunctionComponent`が新たな`React.FunctionComponent`になったのです。したがって、重複する定義`React.VoidFunctionComponent`は残しておく必要がなくなったということでしょう。

```javascript:DefinitelyTyped/types/react/index.d.ts
type FC<P = {}> = FunctionComponent<P>;

interface FunctionComponent<P = {}> {
		(props: P, context?: any): ReactElement<any, any> | null;
		propTypes?: WeakValidationMap<P> | undefined;
		contextTypes?: ValidationMap<any> | undefined;
		defaultProps?: Partial<P> | undefined;
		displayName?: string | undefined;
}

/**
 * @deprecated - Equivalent with `React.FC`.
 */
type VFC<P = {}> = VoidFunctionComponent<P>;

/**
 * @deprecated - Equivalent with `React.FunctionComponent`.
 */
interface VoidFunctionComponent<P = {}> {
		(props: P, context?: any): ReactElement<any, any> | null;
		propTypes?: WeakValidationMap<P> | undefined;
		contextTypes?: ValidationMap<any> | undefined;
		defaultProps?: Partial<P> | undefined;
		displayName?: string | undefined;
}
```

なお、将来は関数コンポーネントの引数となるプロパティは、自動的に`readonly`とされるかもしれません。引数からプロパティを[分割代入](https://qiita.com/FumioNonaka/items/58358a29850afd7a0f37#%E5%88%86%E5%89%B2%E4%BB%A3%E5%85%A5%E3%82%92%E4%BD%BF%E3%81%86)で受け取るとき重要な点でしょう。

参考:「[【検証】React.FC と React.VFC はべつに使わなくていい説](https://kray.jp/blog/dont-have-to-use-react-fc-and-react-vfc/)」


# フック
関数コンポーネントになくてはならないのがフックです。使うときに気をつけなければならない点のあるフックをいくつかご紹介します。

## `useState`
[`useState`](https://github.com/typescript-cheatsheets/react#usestate)フックには、可能なかぎり初期値を与えましょう。そうすれば、状態変数の型は推論されるからです。

```react
const [text, setText] = useState('');  // 与えた初期値によりstringと推論される
```

初期値を与えなかったとき、「[React: フックで値を定めた<input>要素に制御されたコンポーネントを使うよう警告が出る ー Warning: Changing uncontrolled input](https://qiita.com/FumioNonaka/items/0b4771fdce748e0d67ce)」という問題も確認されています。

型を組み合わせたいという場合には、`useState`に[ユニオン型](https://www.fumiononaka.com/Business/html5/FN2104003.html)をジェネリックで定めればよいでしょう。

```react
const [id, setId] = useState<string | number>(0);
```

あるいは、型が定めてあっても、初期値があとの初期化処理で決まるとか、値がない場合として`null`を含めたいということもあるかもしれません。そのときも、ユニオン型で`null`を加えてください。

```react
const [user, setUser] = React.useState<IUser | null>(null);

// 初期化処理など
setUser(newUser);
```

`null`は型に含めず、`useState`の初期値が決まらないという場合には、型アサーションで逃げる手もあります。

```react
const [user, setUser] = React.useState<IUser>({} as IUser);

// 型 'null' の引数を型 'SetStateAction<IUser>' のパラメーターに割り当てることはできません。
// setUser(null);  // nullは認められない
setUser(newUser);  // 型に合致した値のみ設定できる
```

ただし、型アサーションはTypeScriptに値(`{}`)の型を偽っているだけだ、ということにご注意ください。状態変数(`user`)の値を正しく扱うことは、コードの書き手に委ねられるのです。誤ればランタイムエラーになってしまうかもしれません。


## `useReducer`
[`useReducer`](https://github.com/typescript-cheatsheets/react#usereducer)フックを使うには、リデューサの関数が定められていなければなりません。関数に型づけするのは、リデューサが受け取る状態(`state`)とアクション(`action`)のふたつの引数です。戻り値の型は推論されます。なお、アクションの型(`ACTIONTYPE`)は、判別可能なユニオン型で定めました(後述)。

```react
type STORE = { count: number };
type ACTIONTYPE =
	| { type: 'increment' }
	| { type: 'decrement' };
const reducer = (state: STORE, action: ACTIONTYPE) => {
	switch (action.type) {
		case 'increment':
			return { count: state.count + 1 };
		case 'decrement':
			return { count: state.count - 1 };
		default:
			return state;
	}
};

export default reducer;
```

すると、`useReducer`フックを使う側では、とくに型の定めはなくて構いません。

```react
import { useReducer } from 'react';
import reducer from './reducer';

const initialState = { count: 0 };
const Counter = () => {
	const [state, dispatch] = useReducer(reducer, initialState);

};
```

リデューサ関数そのものを、`React.Reducer`で型づけることもできます。状態(`state`)とアクション(`action`)を受け取って、戻り値は状態です。引数の型はジェネリックで与えてください。

```react
import React from 'react';

// const reducer = (state: STORE, action: ACTIONTYPE) => {
const reducer: React.Reducer<STORE, ACTIONTYPE> = (state, action) => {

};
```

なお、「[React + TypeScript: useReducerを使ったカウンターのアプリケーションに型づけする](https://qiita.com/FumioNonaka/items/d8dd1bfed6e39b688933)」は、簡単なカウンターの作例をもとに、さらに詳しく解説しました。ご興味がありましたらお読みください。

### 判別可能なユニオン型
判別に用いるプロパティが備わった型を複数結ぶと、[判別可能なユニオン型](https://typescript-jp.gitbook.io/deep-dive/type-system/discriminated-unions)([discriminated union](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions))と呼ばれます。

```react
type ACTIONTYPE =
	| { type: 'increment' }
	| { type: 'decrement' };
```

判別用のプロパティ値が限定されるため、それ以外の値を弾くことができるのです。

```react
const reducer = (state: STORE, action: ACTIONTYPE) => {
	switch (action.type) {
		case 'increment':
			return { count: state.count + 1 };
		case 'decrement':
			return { count: state.count - 1 };
		// 型 '"reset"' は型 '"increment" | "decrement"' と比較できません。
		/* case 'reset':  // エラー
			return { count: 0 }; */
		default:
			return state;
	}
};
```


## `useEffect`
[`useEffect`](https://github.com/typescript-cheatsheets/react#useeffect)フックでは、はっきりした型づけより、推論される戻り値の型にご注意ください。たとえば、つぎの例です。

```react
useEffect(
     () =>
          window.setTimeout(() => console.log('timeout'), 1000)
     , []
);
```

アロー関数式`=>`の本体に波かっこ`{}`なしに1行で書いた文は、そのまま戻り値になります(「アロー関数」の「[関数の本体](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Functions/Arrow_functions#%E9%96%A2%E6%95%B0%E3%81%AE%E6%9C%AC%E4%BD%93)」参照)。ところが、`useEffect`のコールバック関数は、戻り値はなし(`undefined`)にするか、「[エフェクトのクリーンアップ](https://ja.reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect)」関数でなければなりません。そのため、つぎのようなエラーが示されてしまうのです。

> Type 'number' is not assignable to type 'void | Destructor'.

クリーンアップ関数がないときは、`useEffect`のコールバックから値を返さないように注意してください(本体が1行のときは波かっこ`{}`で括ります)。

```react
useEffect(
	() => {
		window.setTimeout(() => console.log('timeout'), 1000)
	}
	, []
);
```

## `useRef`
[`useRef`](https://github.com/typescript-cheatsheets/react#useref)フックには、3つの型の定め方があります。`current`プロパティが書き替えられるか、`null`値の代入を認めるかどうかが違いです。詳しくは、「[React + TypeScript: useRefの3つの型指定と初期値の使い方](https://qiita.com/FumioNonaka/items/feb2fd5b362f2558acfa)」をお読みください。

```react
// 読み取り専用。
const nullRef = useRef<number>(null);
// 指定した型の値で書き替えられる。nullは不可。
const nonNullRef = useRef<number>(null!);
// 型指定にnullを含める。
const nullableRef = useRef<number | null>(null);
```

# イベントハンドラ
[イベントハンドラ](https://github.com/typescript-cheatsheets/react#forms-and-events)のもっとも簡単な定め方は、要素のイベント属性(`onChange`)に直に書き加えることです。引数のイベント(`event`)の型は推論されますので、注釈が要りません。

```react
import React, { useState } from 'react';

function App() {
	const [text, setText] = useState('');
	return (
		<div>
			<input type="text"
				value={text}
				onChange={(event) => setText(event.currentTarget.value)}
			/>
		</div>
	);
}
```

ハンドラ関数(`handleChange`)を分けて定めると、通常の関数と同じように引数と戻り値を型づけすることになるでしょう(戻り値は推論させても構いません)。

```react
function App() {

	const handleChange = (event: React.FormEvent<HTMLInputElement>): void => {
		setText(event.currentTarget.value);
	};
	return (
		<div>
			<input type="text"
				value={text}
				// onChange={(event) => setText(event.currentTarget.value)}
				onChange={handleChange}
			/>
		</div>
	);
}
```

ハンドラが引数に受け取るイベントの型は、つぎの表のとおりです(「[List of event types](https://github.com/typescript-cheatsheets/react#list-of-event-types)」参照)。

| イベント型       | 説明                                                                                                                                                                                                                                                            |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| AnimationEvent   | CSSアニメーション。 |
| ChangeEvent      | 要素`<input>`、`<select>`、`<textarea>`の値の変更。 |
| ClipboardEvent   | コピー、ペースト、カットを実行。 |
| CompositionEvent | ユーザーがテキストを間接的に入力している(たとえば、キーボードで日本語を入力する場合、OSの設定により文字入力のポップアップウィンドウが表示される)。 |
| DragEvent        | マウスなどのポインティングデバイスによるドラッグ&ドロップ操作 |
| FocusEvent       | 要素がフォーカスを得たか、失った。 |
| FormEvent        | フォームまたはフォーム要素ついて、(1)フォーカスを得たか失った、(2)要素の値が変わった、(3)フォームが送信された。 |
| InvalidEvent     | 入力が有効であることの検証に失敗(たとえば、`<input type="number" max="10">`の要素に20が入力された)。 |
| KeyboardEvent    | キーボードが操作された。ひとつひとつのキーごとに発生する。 |
| MouseEvent       | マウスなどのポインティングデバイスが操作された。 |
| PointerEvent     | 各種ポインティングデバイスが操作された。マウス、ペン/スタイラス、タッチスクリーンなどで、マルチタッチをサポートする。古いブラウザ(Internet Explorer 10やSafari 12)を利用環境に含む開発でないかぎり推奨される。`UIEvent`のサブクラス。 |
| TouchEvent       | タッチデバイスが操作された。`UIEvent`のサブクラス。 |
| TransitionEvent  | CSSトランジション。すべてのブラウザには対応していない。`UIEvent`のサブクラス。 |
| UIEvent          | マウス、タッチ、ポインター操作の基本イベント。 |
| WheelEvent       | マウスホイールまたはそれに相応する入力デバイスによるスクロール(`wheel`と`scroll`は異なるイベントであることに注意)。 |
| SyntheticEvent   | 上記すべてのイベントの基本。イベント型がわからないときに用いる。 |

さらに、ハンドラ関数そのものを(この場合`FormEventHandler`で)型づけることもできます。定められるのは、前の例と同じく引数と戻り値の型です。けれど、それぞれをアラカルトで決めるのでなく、セットにした関数の型で示す方がより明確になります。

```react
function App() {

	// const handleChange = (event: React.FormEvent<HTMLInputElement>): void => {
	const handleChange: React.FormEventHandler<HTMLInputElement> = (event) => {

}
```

ハンドラ関数は、[`DefinitelyTyped/types/react/`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts)でつぎのように型づけされています。基本の型は`EventHandler`です。引数のイベントにより型が分けられています。

```react:DefinitelyTyped/types/react/
//
// Event Handler Types
// ----------------------------------------------------------------------

type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void }["bivarianceHack"];

type ReactEventHandler<T = Element> = EventHandler<SyntheticEvent<T>>;

type ClipboardEventHandler<T = Element> = EventHandler<ClipboardEvent<T>>;
type CompositionEventHandler<T = Element> = EventHandler<CompositionEvent<T>>;
type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>;
type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>;
type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>;
type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>;
type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>;
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>;
type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>;
type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>;
type WheelEventHandler<T = Element> = EventHandler<WheelEvent<T>>;
type AnimationEventHandler<T = Element> = EventHandler<AnimationEvent<T>>;
type TransitionEventHandler<T = Element> = EventHandler<TransitionEvent<T>>;
```

「React+TypeScript Cheatsheets」から基本的な事項を拾い、説明も補いつつコード例とともにご紹介しました。