「TypeScriptを勉強し始めたけどReactでどのように書くのかまではイメージできていない」
「ReactプロジェクトでTypeScriptを導入する方法を知りたい」
「フォーム機能の引数eについての型指定がよくわからない」
今回はReactプロジェクトでTypeScriptを使ったコーディングをする方法について初心者向けに解説します。
この記事では、ReactとTypeScriptの環境構築から、propsや関数の書き方を詳しく説明していきます。
propsの型付けや関数の引数・戻り値の型指定、またフォーム機能で使うe(イベント)に対する型指定まで紹介しますので、最低限ReactでTypeScriptを使う手順がイメージできるようになるはずです。
基本的にはReactとJavaScriptの基本がわかっていれば、理解できるものばかりで「あくまでReactの応用編」くらいなので安心して学んでいってください。
また動画でも解説しているので必要に応じて活用してください。
TypeScriptを使ったReactでのpropsの書き方
まずはpropsの書き方になります。
例題としてTODOリストのアプリを作ることを想定してコンポーネントを書いていきましょう。
import "./App.css";
import TaskList from "./screens/ts-props/TaskList";
function App() {
return (
<>
<TaskList />
</>
);
}
export default App;
import React from "react";
import Task from "./Task";
const TaskList = () => {
return (
<div>
<Task date="20230601" title="aaa" />
<Task date="20230602" title="bbb" />
<Task date="20230603" title="ccc" />
</div>
);
};
export default TaskList;
import React from "react";
const Task = (props) => {
return (
<div>
<p>{props.date}</p>
<p>{props.title}</p>
</div>
);
};
export default Task;
TODOリストを画面に表示するための親コンポーネントでTaskList.tsxがあり、1個のTODOを子コンポーネントのTask.tsxで用意しています。
propsを親→子に渡した場合、子コンポーネントの引数にpropsを書くのですが厳密にはpropsはオブジェクトになっていてプロパティごとに値が入ることになります。
上記の例で言うとdateとtitleという2つのプロパティに値が代入されています。
TypeScriptではそれぞれのプロパティに対して型を指定します。
import React from "react";
// ここを修正
const Task = (props: { date: string; title: string }) => {
return (
<div>
<p>{props.date}</p>
<p>{props.title}</p>
</div>
);
};
export default Task;
さらに現状だとpropsに値を代入するのを固定の値を入力していますが、繰り返し処理など固定の値を書けない場合は親コンポーネントの側でも型の指定が必要になります。
例えばAPIからTODOのデータを取得して画面に表示する方法に変えてみます。
// ここを追加
import React, { useEffect, useState } from "react";
import Task from "./Task";
const TaskList = () => {
// ここを追加
const [todos, setTodos] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/"
);
const json = await response.json();
setTodos(json);
} catch (error) {
console.error(error);
}
};
fetchData();
}, []);
return (
<div>
{/* ここを変更 */}
{todos.map((todo: { id: number; title: string }) => (
<Task key={todo.id} {...todo} />
))}
</div>
);
};
export default TaskList;
あとAPIから使用するプロパティはtitleのみを子コンポーネントにpropsとして渡すように変更しますので、子コンポーネントのTask.tsx側でdateは削除しておいてください。
import React from "react";
// dateを削除した
const Task = (props: { title: string }) => {
return (
<div>
{/* dateを削除した */}
<p>{props.title}</p>
</div>
);
};
export default Task;
APIはjsonplaceholderを使用しています。
https://jsonplaceholder.typicode.com/
APIで取得したデータはmapメソッドを使ってreturn文のなかに書いていきます。
mapメソッドの引数にはAPIから取得したデータが格納されたステート変数を書くわけですが、このmapメソッドの引数について型の指定を書いておくことになります。
そうすることで固定の値じゃない場合も正しくpropsを渡すことが可能になります。
「親でも子でも型を書いて2度手間だな」と思われた方がいるかもしれません。
逆にいうと型の見間違いや値の渡し忘れなどのヒューマンエラーに気づける、というメリットになっているわけです。
通常であればユーザーが問題の処理にぶつかって発見されることが多いエラーが、開発時に気づけるのはリリース後のトラブルの件数を緩和することにつながります。
とはいえ親コンポーネントと子コンポーネントの両方で同じようなことを書くのは手間です。
そこで実務ではReactプロジェクトのなかで何回も使う型のパターンは専用のファイルに分けて使い回すことが多いです。
srcフォルダ直下に新しくtypesフォルダを作り、そのなかにtypes.tsというファイルを新規で作成します。
- node_modules
- public
- src
- types
- types.ts
- App.tsx
- App.css
...そのほかコンポーネントのフォルダなど
先ほどのpropsの型をtypes.tsに改めて書きます。
export type TodoProps = { id: number; title: string };
types.tsにpropsの型ルールをtypeで書いておきエクスポートすることで各コンポーネントで必要に応じて使い回す流れです。
propsの型を直接書くのではなくtypes.tsからインポートする書き方に変えてみましょう。
import React, { useEffect, useState } from "react";
import Task from "./Task";
// ここを追加
import { TodoProps } from "../../types/types";
const TaskList = () => {
const [todos, setTodos] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/"
);
const json = await response.json();
setTodos(json);
} catch (error) {
console.error(error);
}
};
fetchData();
}, []);
return (
<div>
{/* ここを変更 */}
{todos.map((todo: TodoProps) => (
<Task key={todo.id} {...todo} />
))}
</div>
);
};
export default TaskList;
import React from "react";
// ここを追加
import { TodoProps } from "../../types/types";
// ここを変更
const Task = (props: TodoProps) => {
return (
<div>
<p>{props.title}</p>
</div>
);
};
export default Task;
ちなみに親コンポーネントのTaskList.tsx側でmapメソッドの引数に型を指定していますが、今回の場合はuseStateを使っていて初期値に型を書いてもOKです。
import React, { useEffect, useState } from "react";
import Task from "./Task";
import { TodoProps } from "../../types/types";
const TaskList = () => {
// ここに型指定を書いた
const [todos, setTodos] = useState<TodoProps[]>([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/"
);
const json = await response.json();
setTodos(json);
} catch (error) {
console.error(error);
}
};
fetchData();
}, []);
return (
<div>
{/* ここから型指定を削除した */}
{todos.map(todo) => (
<Task key={todo.id} {...todo} />
))}
</div>
);
};
export default TaskList;
さらに子コンポーネントのTask.tsx側でpropsを受け取る際に、propsと書かずに分割代入でプロパティ名だけ取り出す書き方もOKです。
import React from "react";
// ここを追加
import { TodoProps } from "../../types/types";
// 分割代入に変更
const Task = ({ title }: TodoProps) => {
return (
<div>
{/* propsを削除できる */}
<p>{title}</p>
</div>
);
};
export default Task;
画面の表示内容は変わらないのですが、少しでも効率的にコードを書くためにいろんな方法を知っておくと良いでしょう。
今回は詳しく解説しませんでしたがuseStateや分割代入については過去に別の記事で解説しているので必要な方はご覧ください。
propsで値を渡すときにオブジェクト形式だった場合もあります。
オブジェクト形式についてはプロパティの値に型指定を行います。
先ほどのtypes.tsを使って書いてみます。
export type TodoProps = { id: number; title: string };
// オブジェクトを渡すとき
export type UserProps = {
username: {
firstName: string;
lastName: string;
};
followers: {
firstName: string;
lastName: string;
}[];
};
オブジェクトのような入れ子形式のデータであったとしても基本的には値に何が入るかを考えてプロパティごとに型指定するだけです。
ちなみに同じような型の組み合わせであるときはtypes.tsの中で参照することも可能です。
上記のコードだとfirstNameとlastNameというプロパティ名、型の種類ともに同じですが、usernameとfollowersという2つに分けて管理しています。
このようなケースは以下のようにしても同じ意味として使用できます。
export type TodoProps = { id: number; title: string };
// ここを追加
export type NameProps = { firstName: string; lastName: string; };
// ここを修正
export type UserProps = {
username: NameProps;
followers: NameProps[];
};
同じ記載を何回もするのではなく再利用できるものを探せるようになると良いでしょう。
Reactでテンプレートリテラルをpropsで使うときの書き方
文字列のテンプレートリテラルという書き方がありますが結局はstring型であることには変わりませんので以下のように書けばOKです。
import React from "react";
import Toasts from './Toasts';
const App = ({ title }: TodoProps) => {
return (
<div>
<Toasts fullName="山田 太郎" />
</div>
);
};
export default App;
import React from 'react';
type FirstNameProps = string;
type LastNameProps = string;
type NameProps = {
fullName: `${FirstNameProps} ${LastNameProps}`;
};
const Toasts = ({ fullName }: NameProps) => {
return <div>{fullName}</div>;
};
export default Toasts;
propsで動的に変わる部分と全体の文字列を別々のtypeに分けて管理する形で書きました。
そうすると最終的にはfullNameというテンプレートリテラルのpropsを扱うことができるようになります。
またfullNameに渡す値をテンプレートリテラルの書き方に沿わない文字列を入れると、仮に文字列だったとしてもコンパイルエラーにすることができます。
fullNameをシンプルにstring型に型指定するだけでも同じものは作れますが、テンプレートリテラルでより厳格な文字列を指定させることができるため便利です。
import React from "react";
import Toasts from './Toasts';
const App = ({ title }: TodoProps) => {
return (
<div>
<Toasts fullName="山田 太郎" />
{/* テンプレートリテラルに沿わない値は同じ文字列でもコンパイルエラーになる */}
<Toasts fullName="山田太郎" />
</div>
);
};
export default App;
Reactでコンポーネントをpropsで渡す時のTypeScriptの書き方
propsでコンポーネント自体を渡すことがあります。
例えばユーザー認証の機能でログインしていれば管理画面のコンポーネントを表示して、そうでないときはログインを求めるコンポーネントを表示するケースです。
ログインしているときに表示したい管理画面のコンポーネントは以下のように用意します。
ユーザー名をpropsで受け取るシンプルな表示内容です。
import React from 'react';
export type UserProps = {
name: string;
};
const Dashboard = ({ name }: UserProps) => {
return <div>ユーザー名:{name}</div>;
};
export default Dashboard;
一方でログインしていない時の画面は以下のように用意します。
import React from 'react';
const NotLogin = () => {
return <div>ログインしてください</div>;
};
export default NotLogin;
ログイン有無を検証するコンポーネントは以下のように用意します。
isLoginというログイン有無の値を持つpropsと、Componentというログイン時に表示させたいコンポーネントを持つpropsを受け取ります。
isLoginがfalseであればNotLogin.tsxを表示して、isLoginがtrueであれば管理画面であるDashboard.tsxを表示するように条件分岐で分けています。
その際にpropsの型指定をするのですが、ComponentにはDashboard.tsxが入るとしたときにDashboard.tsxはnameという別のpropsを必要とします。
Auth.tsxからするとnameというpropsは送る側になるのでDashboard.tsxからタイプ変数UserPropsを受け取ってComponentに型指定する必要があります。
Componentというpropsがただコンポーネントが入るだけであればReact.Componetで良いのですが、また別のpropsを必要とするならばReact.Component<UserProps>という形で型指定を拡張する必要があるのがポイントです。
import React from 'react';
import NotLogin from './NotLogin';
import { UserProps } from './Dashboard';
type Props = {
isLogin: boolean;
Component: React.ComponentType<UserProps>;
};
const Auth = ({ isLogin, Component }: Props) => {
if (isLogin) {
return <Component name="山田太郎" />;
} else {
return <NotLogin />;
}
};
export default Auth;
これら3点のコンポーネントをApp.tsxで使うには以下のように書けばOKです。
import React from 'react';
import Auth from './Auth';
import Dashboard from './Dashboard';
const App = () => {
// ユーザーがログインしているかどうかで表示するかを制御するためのAuthコンポーネントに渡して検証してもらう作業
return (
<div>
<Auth isLogin={true} Component={Dashboard} />
</div>
);
};
export default App;
TypeScriptのジェネリクスTを使って配列のpropsの受け取り方
基本的にはpropsというものは想定される値の型名を指定するわけですが、配列になると少しだけ毛色が変わってきます。
例えば以下のような文字列の配列と文字列を使う関数の2つのpropsを受け取るケースがあったとします。
import React from 'react';
type ListProps = {
items: string[];
onClick: (value: string) => void;
};
const Lists = ({ items, onClick }: ListProps) => {
return (
<div>
<h2>選択してください</h2>
{items.map((item, index) => {
return (
<div key={index} onClick={() => onClick(item)}>
{item}
</div>
);
})}
</div>
);
};
export default Lists;
import React from 'react';
import Lists from './Lists';
const App = () => {
const users = ['yamada', 'tanaka', 'yoshida'];
const areas = ['tokyo', 'osaka', 'nagoya'];
return (
<div>
<Lists items={users} onClick={(item) => console.log(item)} />
<Lists items={areas} onClick={(item) => console.log(item)} />
</div>
);
};
export default App;
itemsについては配列usersもしくはareasが渡されることになっていて、ともに中身の要素は文字列ですのでLists.tsxの中でstring[ ]という型指定をしました。
このように配列の型名は中身の要素の型名も指定することになります。
またitemsを元にしたonClick関数もpropsで渡していて、itemsの内容を使うので関数に渡す引数は文字列になります。
そのためonClick関数についても(value: string) => voidという型指定をしておけばコンパイルエラーにはなりません。
上記コードではitemsに入る配列がシンプルな文字列の想定ですが、もし以下のようにオブジェクトの配列が来るとなったらstring[ ]という型指定から外れるのでコンパイルエラーになります。
import React from 'react';
import Lists from './Lists';
const App = () => {
// ここを変更
const students = [
{
name: 'yamada',
age: 20,
gender: 'female',
},
{
name: 'tanaka',
age: 21,
gender: 'male',
},
{
name: 'yoshida',
age: 22,
gender: 'male',
},
];
return (
<div>
{/* ここを変更 */}
<Lists items={students} onClick={(item) => console.log(item)} />
</div>
);
};
export default App;
itemsに入る配列の中身はオブジェクトでプロパティの値も文字列だったり数値だったりする状況です。
そうした複雑な要素を持つ配列の場合にはTypeScriptにあるジェネリクスというものを使います。
ジェネリクスは明確な型名を指定しなくても柔軟にいろんなタイプの型を許容するための書き方で、Lists.tsxは以下のように書くことになります。
import React from 'react';
// ここを変更
type ListProps = {
items: T[];
onClick: (value: T) => void;
};
// ここを変更
const Lists = <T extends { name: string; age: number; gender: string }>({ items, onClick }: ListProps<T>) => {
return (
<div>
<h2>選択してください</h2>
{items.map((item, index) => {
return (
<div key={index} onClick={() => onClick(item)}>
{item.name}
</div>
);
})}
</div>
);
};
export default Lists;
Tという記載をすることでジェネリクスを使う合図になっていて、T[ ]という型名は中身がなんでもOKな配列を指定することになっています。
またitemsの中身を使ったonClick関数についても(value: T) => voidとすることでitemsの中身が文字列でない場合でも引数として受け取れるようになります。
さらにpropsの書き方としてはジェネリクスを使うときは<T extends {オブジェクトの中身}>({ props名 })という書き方に変更することになります。
ただしreturnの部分でitem.nameというオブジェクトの中から特定のプロパティの値を取得するため、プロパティごとの型名は<T extends {name: string; age: number; gender: string;}>と言ったように指定する必要があります。
ジェネリクスについての知識がある前提ですが、もしジェネリクスを詳しく知りたい方は以下の記事にまとめていますのでご覧ください。
TypeScriptを使ったReactでのイベントと関数の書き方
続いてイベントと関数の書き方を紹介します。
基本的には引数と戻り値に型を指定するだけなので書き方さえ覚えればOKなことが多いです。
例題は少し変えてTODOリストを入力する画面を作っていきます。
import "./App.css";
// ここを変更
import Form from "./screens/ts-event/Form";
function App() {
return (
<>
{/* ここを変更 */}
<Form />
</>
);
}
export default App;
import React from "react";
const Form = () => {
return (
<div>
<form>
<input type="text" onChange={handleChange} />
<button onClick={handleClick}>検索</button>
</form>
<div>
<div>
<span>掃除</span>
<button onClick={() => deleteTodo(1)}>完了</button>
</div>
<div>
<span>料理</span>
<button onClick={() => deleteTodo(2)}>完了</button>
</div>
<div>
<span>買い物</span>
<button onClick={() => deleteTodo(3)}>完了</button>
</div>
</div>
</div>
);
};
export default Form;
このようなフォームにおいてinputタグとbuttonタグにそれぞれ関数を作っていきます。
・入力欄に入力したときの関数:handleChange
・検索ボタンをクリックしたときの関数:handleClick
・完了ボタンをクリックしたときの関数:deleteTodo
import React from "react";
const Form = () => {
// ここを追加
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
console.log("クリック");
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const deleteTodo = (id: number) => {
console.log(`${id}を削除`);
};
return (
<div>
<form>
<input type="text" onChange={handleChange} />
<button onClick={handleClick}>検索</button>
</form>
<div>
<div>
<span>掃除</span>
<button onClick={() => deleteTodo(1)}>完了</button>
</div>
<div>
<span>料理</span>
<button onClick={() => deleteTodo(2)}>完了</button>
</div>
<div>
<span>買い物</span>
<button onClick={() => deleteTodo(3)}>完了</button>
</div>
</div>
</div>
);
};
export default Form;
handleDeleteの引数は数字にしていますが、こちらは実際の仕様によりますので適した型を指定します。
初学者の方が慣れないのはeの型だと思います。
何やら難しい単語が書かれていますが「イベントの型<イベントが発生する要素の型>」という感じです。
イベントというのは「クリック」「入力」などのことで、これらのイベントの種類にはそれぞれ事前に型名が決められています。
さらにイベントが発生する場所、つまりHTML要素もinputタグやbuttonタグなどがあり事前に型名が決められています。
それぞれ膨大な種類があり事前に暗記することは現実的ではないので、学習や開発を通じて1個ずつ覚えるか調べるような形になるでしょう。
とはいえ頻出のものは何回も使っているうちに覚えられるので安心してください。
初学者の方は「eにも型を書かないといけない」とだけ覚えておきましょう。
前章でやったpropsに関数を渡すこともありますが考え方は同じです。
import React from 'react';
import Button from './components/Button';
const App = () => {
return (
<div>
<Button click={(e, text) => alert(text)} />
</div>
);
};
export default App;
buttonタグを持つButton.tsxにeを引数にとる関数を渡しました。
propsなのでtypeを使って受け取る関数の引数と戻り値の型を指定します。
引数はbuttonタグに向けたクリックイベントのeなのでReact.MouseEvent<HTMLButtonElement>という書き方になります。
また今回のように戻り値に型指定がないときvoidにします。
import React from 'react';
type BtnProps = {
click: (e: React.MouseEvent<HTMLButtonElement>, text: string) => void;
};
const Button = (props: BtnProps) => {
return (
<div>
{/* <button onClick={props.click}>Button</button> */}
<button onClick={(e) => props.click(e, 'おはよう')}>Button</button>
</div>
);
};
export default Button;
続いてinputタグを持つInput.tsxにもeを持つ関数をpropsで渡してみます。
import React from 'react';
// ここを変更
import Input from './components/Input';
const App = () => {
return (
<div>
{/* ここを変更 */}
<Input change={(e) => console.log(e.target.value)} />
</div>
);
};
export default App;
inputタグにチェンジイベントのeを渡すので引数はReact.ChangeEvent<HTMLInputElement>という指定になります。
import React from 'react';
type InputProps = {
change: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
const Input = (props: InputProps) => {
return (
<div>
<input type="text" onChange={props.change} />
</div>
);
};
export default Input;
TypeScriptでReactのCSSスタイルをpropsで渡す方法
propsはCSSを渡すことでも使われます。
そもそもReactにおけるCSSはHTMLタグにstyle属性で指定するものなので以下のような書き方になります。
import React from 'react';
import Title from './components/Title';
const StyleProps = () => {
return (
<div>
StyleProps
<Title styles={{ fontSize: '40px', color: 'red'}} />
</div>
);
};
export default Input;
import React from 'react';
type TitleStyleProps = {
styles: string;
};
const Title = (props: TitleStyleProps) => {
return (
<div>
<h1 style={props.styles}>タイトル</h1>
</div>
);
};
export default Title;
JavaScriptではCSSを文字列で扱うため型名をstringにして、あとは複数あるプロパティをオブジェクトで囲んで渡しています。
しかし実務ではこのような使い方はしません。
文字列であればCSSとは関係ない文字を書いてもコンパイルエラーで防げないからです。
そのため以下のようにすることが一般的です。
import React from 'react';
type TitleStyleProps = {
// ここを修正
styles: React.CSSProperties;
};
const Title = (props: TitleStyleProps) => {
return (
<div>
<h1 style={props.styles}>タイトル</h1>
</div>
);
};
export default Title;
React.CSSPropertiesという特別な型が用意されています。
こちらであれば以下のようにCSSに関係ない文字列を渡そうとしてもコンパイルエラーで防ぐことができます。
import React from 'react';
import Title from './components/Title';
const StyleProps = () => {
return (
<div>
StyleProps
{/* CSSとは関係ないtestという文字列を含めてみる */}
<Title styles={{ fontSize: '40px', color: 'red', test: 'test'}} />
</div>
);
};
export default Input;