290
267

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-03-19

ReactでTypeScriptを使おうとすると、TypeScriptの基礎知識だけでは足りず、Reactの型について知らなければなりません。それで、戸惑うことも多いでしょう。そういう人たちのためにReactとTypeScriptの使い方をまとめたのが「React+TypeScript Cheatsheets」です。

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

関数コンポーネント

まずは、関数コンポーネントです。普通の関数と同じく、引数を型づけしてください。JSXの要素を返せば戻り値は推論されます。

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

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

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

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

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

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

ただし、コンポーネントの関数をReact.FunctionComponentまたはReact.FCで型づけするのは、これからはReact 17まではお勧めできません。これらの型がTypeScriptの基本テンプレートから除かれるからです。

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

Reactの関数コンポーネントであることをはっきりと示したいなら、React 17まではReact.VoidFunctionComponent(React.VFC)、React 18からはReact.FunctionComponent(React.FC)を使うのがよいでしょう。ただし、プロパティにchildrenを受け取るときは、型定義に含めなければなりません。その場合の型はReact.ReactNodeです。

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によれば、つぎのようにReact.FunctionComponentの定めはReact.VoidFunctionComponentと同じになりました。つまり、React.VoidFunctionComponentが新たなReact.FunctionComponentになったのです。したがって、重複する定義React.VoidFunctionComponentは残しておく必要がなくなったということでしょう。

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とされるかもしれません。引数からプロパティを分割代入で受け取るとき重要な点でしょう。

参考:「【検証】React.FC と React.VFC はべつに使わなくていい説

フック

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

useState

useStateフックには、可能なかぎり初期値を与えましょう。そうすれば、状態変数の型は推論されるからです。

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

初期値を与えなかったとき、「React: フックで値を定めた要素に制御されたコンポーネントを使うよう警告が出る ー Warning: Changing uncontrolled input」という問題も確認されています。

型を組み合わせたいという場合には、useStateユニオン型をジェネリックで定めればよいでしょう。

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

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

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

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

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

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

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

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

useReducer

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

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フックを使う側では、とくに型の定めはなくて構いません。

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

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

};

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

import React from 'react';

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

};

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

判別可能なユニオン型

判別に用いるプロパティが備わった型を複数結ぶと、判別可能なユニオン型(discriminated union)と呼ばれます。

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 };
		// 型 '"reset"' は型 '"increment" | "decrement"' と比較できません。
		/* case 'reset':  // エラー
			return { count: 0 }; */
		default:
			return state;
	}
};

useEffect

useEffectフックでは、はっきりした型づけより、推論される戻り値の型にご注意ください。たとえば、つぎの例です。

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

アロー関数式=>の本体に波かっこ{}なしに1行で書いた文は、そのまま戻り値になります(「アロー関数」の「関数の本体」参照)。ところが、useEffectのコールバック関数は、戻り値はなし(undefined)にするか、「エフェクトのクリーンアップ」関数でなければなりません。そのため、つぎのようなエラーが示されてしまうのです。

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

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

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

useRef

useRefフックには、3つの型の定め方があります。currentプロパティが書き替えられるか、null値の代入を認めるかどうかが違いです。詳しくは、「React + TypeScript: useRefの3つの型指定と初期値の使い方」をお読みください。

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

イベントハンドラ

イベントハンドラのもっとも簡単な定め方は、要素のイベント属性(onChange)に直に書き加えることです。引数のイベント(event)の型は推論されますので、注釈が要りません。

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)を分けて定めると、通常の関数と同じように引数と戻り値を型づけすることになるでしょう(戻り値は推論させても構いません)。

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」参照)。

イベント型 説明
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 マウスホイールまたはそれに相応する入力デバイスによるスクロール(wheelscrollは異なるイベントであることに注意)。
SyntheticEvent 上記すべてのイベントの基本。イベント型がわからないときに用いる。

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

function App() {

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

}

ハンドラ関数は、DefinitelyTyped/types/react/でつぎのように型づけされています。基本の型はEventHandlerです。引数のイベントにより型が分けられています。

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」から基本的な事項を拾い、説明も補いつつコード例とともにご紹介しました。

290
267
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
290
267

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?