【初心者】React × TypeScript 基本の型定義
はじめに
ここ最近TypeScriptの学習をしていまして、その学習記録をZennに投稿し続けていました。
その中で、TypeScriptの基礎学習の最後として投稿した以下の記事では、TypeScriptを用いてReact開発をする際に最低限必要となるであろうTypeScriptの型について簡単にまとめました。
先述の記事を書いている際、TypeScriptを用いたReactの基本的な型定義について網羅的にまとめている記事がまだまだ多くないように感じたため、今回「React × TypeScriptの基本の型定義」について改めてまとめ直してみることにしました。
TypeScriptの基礎学習を終え、これからTypeScriptを利用してReactやNext.jsでの開発をしてみようという方の参考になれば幸いです。
そこそこ長くなってしまったため、適宜必要な部分のみ参考にしていただくのがいいかもしれません。
TypeScriptを学習し始めて2週間程度の知識なので、間違いや修正点、追加したほうがいいことなどあれば是非是非コメントして頂けると嬉しいです。
注意書き
今回扱わないこと
この記事では、以下のことについては扱いません。
この記事を読む上で最低限必要となる React と TypeScript の「基礎」って一体どこまで?
上で「ReactやTypeScriptの基礎的な内容は扱いません」と注意書きを入れていますが、「じゃあこの記事を読む上で最低限どこまで知っていればいいんだ」という声が聞こえてきそうなので、以下にその目安を記しておきます。是非参考にしてみてください。
この記事を読み進める上での最低限必要な「基礎理解」とは以下の様な内容を指しています。
この記事が求める「React」の基礎理解
-
JSX記法
がどういうものかわかる -
関数コンポーネント
の書き方がわかる -
ステート(state)
が何かわかる -
props
が何かわかる -
propsの渡し方・受け取り方
がわかる -
useState
の使い方がわかる -
イベントオブジェクト(event)
が何かわかる -
onClick
やonChange
などのイベントハンドラの使い方がわかる
この記事が求める「TypeScript」の基礎理解
-
型推論
が何かわかる -
型注釈(型アノテーション)
が何かわかる -
プリミティブ型の型定義
がわかる -
配列の型定義
がわかる -
オブジェクトの型定義
がわかる -
合併型(Union Type)
がどういうものかわかる -
型エイリアス(Type Alias)
での型定義の仕方がわかる -
ジェネリック型<T>
がどういうものかわかる
上記の挙げたことで分からないことがある場合は、ぜひ上の赤枠の中に載せてある動画などを用いて React や TypeScript の基礎学習から始めてみてください😌
とてもわかりやすいのでおすすめです!
環境
- React 17.0.2
- Next.js 10.2.0
- TypeScript 4.2.4
- @types/react 17.0.5
Reactに関わる基本的な型定義
TypeScriptを使用してのReact(Next.js)開発で、最低限知っておいた方がいいであろう型定義について解説していきます。
1. 型のインポート
React用の型をインポートする際は、以下の様に書きます。
// 型のインポート
import React from "react"
// 実際の使用例(詳しくは後述)
const SampleComponent: React.VFC = () => {
return <div>Hello TypeScript!</div>
}
また、以下の様にimport type { 特定の型 }
と書くことで、「型情報のみのインポート」をすることができます。より詳しく知りたい方はこちらの記事が参考になります。
// 型のインポート
import type { VFC } from "react"
// 実際の使用例
const SampleComponent: VFC = () => {
return <div>Hello TypeScript!</div>
}
個人的にはimport type
を使用した「型のみのインポート」の方がシンプルで好きです。
ですが、この記事では分かりやすさを重視して、見慣れている方が多いであろうimport React from "react"
のタイプで統一します。
省略したい方は適宜React.VFC
などのReact.
の部分を適宜省略する形で読み替えて頂けると幸いです。
2. 関数コンポーネントの型定義
関数コンポーネントの型定義にはReact.VFC
という型を用います。
VFC
はVoid Function Component
の略です。
基本的な使い方としては、コンポーネント名に対して型注釈する形で型を指定します。
import React from "react"
// 実際の使用例
const SampleComponent: React.VFC = () => {
return <div>Hello TypeScript!</div>
}
React.VFC に似た React.FC とは?
React.VFC
に似た型として、React.FC
という型があります。
const SampleComponent: React.FC = () => {
return <div>Hello TypeScript!</div>
}
詳しくはこちらの記事がわかりやすいので参考にして頂きたいですが、簡単に違いを述べると、React.FC
では、「型定義の中に暗黙的にchildren
を含んでしまっている」という問題があります。
これがどういうことかと言うと、children
を使う予定のないコンポーネントだとしても、children
を受け取ることができてしまい、TypeScriptの良さが損なわれてしまうということです。
もう少しわかりやすく言うと、TypeScriptでは受け取るpropsに型を定義して予期せぬpropsが渡ってくることを事前に検知して防ぐことができるのですが、React.FC
ではその受け取るpropsの型定義の中にchildren
を定義していないのに、children
を渡してもエラーにならないということですね。
React.VFC
では明示的にchildren
を型定義していない場合、children
を渡そうとしてもちゃんとエラーになります。
このため、現在ではReact.VFC
の方が推奨されており、基本的にはReact.VFC
を用いるのがベターだと言えます。
propsとしてchildren
を受け取る際は、propsの型定義の中に明示的にchildren
を含めるようにします(後述)。
また、今後のReact ver.18で、React.FC
から暗黙的なchildren
の型定義がなくなるとのことです。
3. propsの型定義
Reactではコンポーネントがpropsを受け取ることが多々あるかと思います。
その際、propsとして「どんな値を受け取ることができるのか」を予め定義しておくことで、予期せぬpropsが渡ってくることを事前に検知して防ぐことができます。
ここではpropsの型定義の方法について紹介します。
propsの型定義の方法はいくつかのパターンがあるので、以下ではそれらを紹介していきます。
3-1. propsを受け取らない場合
propsを受け取る場合の型定義の前に、「propsを受け取らない」場合の型定義について見ていきます。
propsを受け取らない場合は以下の2パターンがあります。
import React from "react"
// ① 型推論に任せるパターン
const SampleComponent1 = () => {
return <div>Hello TypeScript!</div>
}
// ② 型注釈を付けるパターン
const SampleComponent2: React.VFC = () => {
return <div>Hello TypeScript!</div>
}
個人的にはpropsを受け取らない場合でもReact.VFC
で型定義しておきたい派(②)ですが、どちらで書いても構いません。
3-2. propsを受け取る場合
本題である「propsを受け取る」場合の型定義について見ていきます。
propsを受け取る場合も大きく2パターンの型の指定方法があります。
-
props
に直接型注釈を指定するパターン -
React.VFC<P>
のジェネリック型<P>
として型を指定するパターン
どちらのパターンで書いても結果は変わりません。
import React from "react"
// props として受け取る型の定義(`Props`部分の名前はどんな名前でも可)
type Props = {
text: string
}
// ③ props に直接型注釈を指定するパターン
const SampleComponent3 = (props: Props) => {
return <div>Hello {props.text}!</div>
}
// ④ React.VFC<P>のジェネリック型<P>として型を指定するパターン
const SampleComponent4: React.VFC<Props> = (props) => {
return <div>Hello {props.text}!</div>
}
/* ---------- 呼び出す側 ---------- */
const Parent: React.VFC = () => {
return (
<div>
{/* ③も④も結果は同じ。propsが不足していたり型が違うものを渡すなどするとエラーになる */}
<SampleComponent3 text="TypeScript" />
<SampleComponent4 text="TypeScript" />
</div>
)
}
React.VFC<P> の <P> について
React.VFC<P>
の<P>
はpropsの型を受け取るジェネリック型(ジェネリクス)です。
React.VFC<P>
の型定義を見てみると、type VFC<P = {}> = VoidFunctionComponent<P>
とあり、<P>
のデフォルト値として{}
が指定されています。
type VFC<P = {}> = VoidFunctionComponent<P>;
interface VoidFunctionComponent<P = {}> {
(props: P, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
これまでに度々出てきているReact.VFC
は、React.VFC<P>
の<P>
を指定せずにデフォルト値{}
が代入されたものということですね。
ここまでのまとめ
ここまでで見てきた「型のインポート」、「関数コンポーネントの型定義」、「propsの型定義」について以下に簡潔にまとめました。
// 以下において、`①`と`②`は同じ。`③`と`④`は同じ。
// 個人的には`②`と`④`をよく使う印象
import React from "react"
type Props = {
text: string
}
// ① propsなし 型推論に任せるパターン
const SampleComponent1 = () => {
return <div>Hello TypeScript!</div>
}
// ② propsなし 型注釈を付けるパターン
const SampleComponent2: React.VFC = () => {
return <div>Hello TypeScript!</div>
}
// ③ propsあり propsに直接型注釈パターン
const SampleComponent3 = (props: Props) => {
return <div>Hello {props.text}!</div>
}
// ④ propsあり ジェネリック型<P>で型指定パターン
const SampleComponent4: React.VFC<Props> = (props) => {
return <div>Hello {props.text}!</div>
}
補足
長くなり過ぎてしまったので、以下の事柄についてはアコーディオンとして以下にまとめてあります。
必要に応じて参照して頂けると幸いです。
【補足】propsとして children を受け取る場合の型定義
children
を受け取る場合、children
の型にはReact.ReactNode
という型を指定します。
import React from "react"
type Props = {
text: string
children: React.ReactNode
}
const SampleComponent5: React.VFC<Props> = (props) => {
return (
<div>
<h1>Hello {props.text}!</h1>
<p>{props.children}</p>
</div>
)
}
/* ---------- 呼び出す側 ---------- */
const Parent: React.VFC = () => {
return (
<SampleComponent5 text="TypeScript">
絶対可憐チルドレン
</SampleComponent5>
)
}
【補足】propsの数が1~2個でわざわざprops用に型を宣言したくない場合の書き方
propsとして受け取る値の型について、propsの要素数が1つや2つで、わざわざProps
型として型エイリアスを作るまでもない場合は、以下の様にReact.VFC
のジェネリック型<P>
に直接型を指定することもできます。
import React from "react"
// ジェネリック型<P>の部分に直接`props`が受け取る型を指定することができる
// 2つ以上の型をワンラインで書く場合は以下のように属性の区切りをカンマで区切る
const SampleComponent5: React.VFC<{ text: string, children: React.ReactNode }> = (props) => {
return (
<div>
<h1>Hello {props.text}!</h1>
<p>{props.children}</p>
</div>
)
}
個人的には保守性や拡張性の観点から、propsの数がたとえ少なかったとしても型エイリアスでpropsの型宣言をしておく方が無難なのかなと思ったりはしますが、ここは好みによるのでしょうか。
【補足】propsを分割代入で受け取る場合の型定義
Reactに慣れてくると分割代入を使いたくなる場面は多いかと思います。
propsを分割代入で受け取る場合の型定義は以下の様になります。
import React from "react"
type Props = {
text: string
}
// ③ propsあり propsに直接型注釈パターン
const SampleComponent3 = ({ text }: Props) => {
return <div>Hello {text}!</div>
}
// ④ propsあり ジェネリック型<P>で型指定パターン
const SampleComponent4: React.VFC<Props> = ({ text }) => {
return <div>Hello {text}!</div>
}
【おまけ】propsとして受け取る値の型定義色々
ここまで述べてきたpropsの型定義では、propsとして受け取る値がstring型
の場合しか扱っていませんでしたが、それ以外の型は以下の様に指定することができます。
TypeScriptの基本的な型定義が理解できていれば特に難しいことはないかと思います。
// propsとして受け取る値の型定義色々
type Props = {
str: string // 文字列
num: number // 数値
bool: boolean // 真偽値
strArr: string[] // 配列
obj: { // オブジェクト
str: string
}
objArr: { // オブジェクトの配列
str: string
num: number
}[]
func: () => void // 関数
}
4. useStateの型定義
Reactでも登場頻度の高いフック(hook)であるuseState
の型定義について見ていきます。
useStateの型定義は、useStateを宣言する時に指定します。
useStateの型定義の方法は、ステートの内容がどういうものであるかによって変わるので以下で1つずつ見ていきます。
4-1. 型推論に任せる
useStateに初期値を設定する場合、初期値から型が明確であれば型推論によって型を推測してくれるため、無理に型をつける必要はありません。
// 初期値から型が明確な場合は型推論に任せる
const [text, setText] = useState("") // string型
const [count, setCount] = useState(0) // number型
const [isShow, setIsShow] = useState(false) // boolean型
// 配列の場合
const [animals, setAnimals] = useState(["dog", "cat"]) // string型の配列
const [numbers, setNumbers] = useState([1, 2, 3]) // number型の配列
// オブジェクトの場合
// 以下は { name: string型, age: number型, isMarried: boolean型 } というオブジェクトの型がuserステートに自動で付く
const [user, setUser] = useState({ name: "aiko", age: 45, isMarried: false})
4-2. useStateの型定義(プリミティブ値)
ここからは、useStateに対して明示的に型定義をする方法について見ていきます。
useStateに対して明示的に型を指定する場合は、ジェネリック型<T>
を用いて型を指定します。
プリミティブ値
とは、JavaScriptにおける基本的な値のことで、簡単に言えば文字列
、数値
、真偽値
などのことです。
ここではまず始めに、ステートの値がプリミティブ値
の場合について見ていきます。
要するに、ステートとして保持する値が配列やオブジェクトではない場合の話ですね。
ステートの値がプリミティブ値
である場合は以下の様に型を指定します。
// useStateのジェネリック型<T>に明示的に型を指定する
const [text, setText] = useState<string>("") // string型
const [count, setCount] = useState<number>(0) // number型
const [isShow, setIsShow] = useState<boolean>(false) // boolean型
// nullを含む場合は、合併型(Union Type)を用いて複数の型を指定する
const [count, setCount] = useState<number | null>(null) // number型もしくはnull型
上記例の中で最後に書いてあるような、nullを含む場合でなければ、基本的にプリミティブ値の型定義は型推論に任せて良いと思っています。
4-3. useStateの型定義(配列)
ステートの値が配列
である場合の型の指定方法です。ジェネリック型<T>
に直接配列の型を指定します。
// useStateのジェネリック型<T>に直接配列の型を指定する
const [animals, setAnimals] = useState<string[]>([]) // string型の配列
const [numbers, setNumbers] = useState<number[]>([]) // number型の配列
// 文字列と数値など、複数の値の配列を受け取る場合は以下の様に型を指定する
const [hoge, setHoge] = useState<(string | number)[]>([]) // string型もしくはnumber型の値を受け取る配列
4-4. useStateの型定義(オブジェクト)
ステートの値がオブジェクト
である場合の型の指定方法です。
ステートにオブジェクトを持たせる場合は、予め型エイリアスなどで型を定義しておき、その型をジェネリック型<T>
で指定します。
またこの時、useStateの初期値は指定した型の構造を満たしている必要があります。
import React from "react"
// ステートが持つオブジェクトの構造を型定義
type UserData = {
id: number
name: string
}
const StateSample: React.VFC = () => {
// useStateのジェネリック型<T>に、上で定義した`UserData型`を指定
const [user, setUser] = useState<UserData>({}) // NG 初期値が型の構造を満たしていないのでエラー
const [user, setUser] = useState<UserData>({ id: 1122, name: "aiko" }) // OK
return (
<div>
<h1>{user.id}</h1>
<h1>{user.name}</h1>
</div>
)
}
4-5. useStateの型定義(オブジェクトの配列)
ステートの値がオブジェクトの配列
である場合の型の指定方法です。
以下ではTodo
という型(データ構造)に沿ったオブジェクトを格納する配列として、useStateの型にTodo[]
を指定しています。
import React from "react"
// ステートが持つオブジェクトの構造を型定義
type Todo = {
id: number
body: string
}
const StateSample: React.VFC = () => {
// `Todo`という型に沿うオブジェクトの配列を、useStateのジェネリック型<T>として型指定( Todo[] の部分)
const [todos, setTodos] = useState<Todo[]>([{}]) // NG 初期値が型の構造を満たしていないのでエラー
const [todos, setTodos] = useState<Todo[]>([]) // OK 空配列はOK
const [todos, setTodos] = useState<Todo[]>([{ id: 1, body: "この記事を書き上げる" }]) // OK
return (
<ul>
{todos.map((item: Todo) => {
return <li key={item.id}>{item.body}</li>
})}
</ul>
)
}
5. イベントオブジェクト(event)の型定義
ReactではonClick
やonChange
などのイベントハンドラを用いて、イベントオブジェクト(event
)を扱うことも多くあるかと思います。
最後に、イベントハンドラやイベントオブジェクトの型定義の方法について見ていきます。
イベントハンドラ / イベントオブジェクトの型定義
onClick
やonChange
などのイベントハンドラ、そしてイベントオブジェクト(event
)の型定義は以下の様な形になります(あくまで一例です)。
// `handleChange`などの関数を定義する際のイベントオブジェクトの型定義(この章の最後に出てきます)
const [inputText, setInputText] = useState("")
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputText(event.target.value)
}
// propsとして`onClick`などのイベントハンドラを子コンポーネント側で受け取る時の型定義
type Props = {
handleClick: (event: React.MouseEvent<HTMLInputElement>) => void // onClick
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void // onChange
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void // onSubmit
}
イベントオブジェクトの型を知る方法
上でイベントオブジェクトなどの型定義を見ましたが、イベントオブジェクト(event
)の型が少し特殊なことに気付いたかと思います。また、イベントハンドラの種類によってもイベントオブジェクトの型が異なっていることにも気付いたかと思います。
上のようなイベントオブジェクトの型はどのようにして知ればよいのでしょうか。
ここではまず「イベントオブジェクトの型をどのようにして知るか」について解説していきます。
イベントオブジェクトの型については以下の記事が大変参考になります。
上記の参考記事の中ではイベントオブジェクトの型を知る方法として、以下の様に書いてあります。
自分は普段、VSCode を使っているため、迷ったときは VSCode のコードヒントに頼ることにしています。つぎのキャプチャは、onClick まで空で書いて、 onClick にマウスオーバーした時のものです。
ただ、自分が使っているVSCodeの問題なのか、onClick
やonChange
にマウスを乗せてみても、記事の画像とは異なりイベントオブジェクトの型情報は表示されませんでした。
参考記事の通りにマウスを乗せると自分のVSCode上ではこのように表示される
イベントオブジェクトの型情報が表示されて欲しいところですが、これでは少し困ってしまいます。
どうにかならないかと色々といじっていたところ解決策を見つけたので以下で解説します。
onClick
などを書いた後( onClick={}
の後 )、引数を渡すための丸括弧()
を打つとイベントオブジェクトの型推論が出てくるのでこちらを参照します(下の画像参照)。
上がonClick
、下がonChange
のイベントオブジェクトの型
自分と同じような状況の方はこちらの方法でイベントオブジェクトの型情報を参照すると良いかと思います。
これでイベントオブジェクト(event
)の型を知ることができました。
イベントハンドラ / イベントオブジェクトの実際の使用例
最後にonChange
などのイベントハンドラやイベントオブジェクト(event
)のTypeScriptでの実際の使用例を見て終わりにします。
以下は、テキストフォームに文字を入力し、その入力された文字列を画面に反映させる例です(よくあるやつですね)。
入力フォームをコンポーネントとして分割し、onChange
で用いるhandleChange
関数などをpropsとして親コンポーネントから渡しています。
import React from "react"
/* ---------- 親コンポーネント(propsを渡す側) ---------- */
const Parent: React.VFC = () => {
const [inputText, setInputText] = useState("")
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputText(event.target.value)
}
return (
<div>
<h1>{inputText}</h1>
<InputTextForm inputText={inputText} handleChange={handleChange} />
</div>
)
}
/* ---------- 子コンポーネント(propsを受け取る側) ---------- */
type Props = {
inputText: string
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void
}
const InputTextForm: React.VFC<Props> = (props) => {
return (
<div>
<input
type="text"
value={props.inputText}
onChange={props.handleChange}
/>
</div>
)
}
おわりに
最初はそこまで長くならないだろうと思っていたのですが、色々と検証したり調べたりしているうちに相当長くなってしまいました…。
わかりやすい記事にするのは中々難しいですね。
少しでも参考になった点があれば嬉しいです。
読んでいただきありがとうございました😌
最後に余談ですが、できる限り見やすい記事にするために、Zennの記事の中でもトップレベルにわかりやすいと個人的に思っている【ねこアレルギー | NekoAllergy】さんの記事の構成をとても参考にしました。まだまだ遠く及びませんが、今後は画像なども使えるようになってよりわかりやすい記事が書けたらなと思います。
追記(5/29)
想像以上にたくさんの方に見てもらえてとても嬉しいです。
この記事に目を通してくれた方、本当にありがとうございます。
この記事は、記事を公開した後も恐らく20~30回は推敲と校正を繰り返して改善していたりします。
今後も、TypeScriptを用いてReact(Next.js)で開発を始めてみようという方のはじめの一歩として参考になり続けてくれたら嬉しいです。
これからも一緒に頑張りましょう💪
追記(6/2)
morimorig3さんからコメントして頂いた内容をもとに、「useStateの型定義(プリミティブ値)」の部分の内容を更新しました。
コメントして頂きありがとうございました!🙇♀️
参考
オンライン家庭教師マナリンクを運営するスタートアップNoSchoolのテックブログです。 manalink.jp/ 創業以来年次200%前後で売上成長しつつ、技術面・組織面での課題に日々向き合っています。 カジュアル面談はこちら! forms.gle/fGAk3vDqKv4Dg2MN7
Discussion
コメント失礼します。
綺麗にまとめられており、理解しやすかったです!このテーマだと膨大な量になりがちですが、要点を抑えてコンパクトにまとめるのが上手ですね。続きがあればぜひ読んでみたいです!
1点お節介ですが、こちらプリミティブ値のことではないかな?と思いました!
morimorig3さん、コメントありがとうございます!
ありがとうございます!こういった感想を頂けるのが何より嬉しいです☺️
お節介なんてとんでもないです!むしろ待ちわびていました笑
なるほど、
プリミティブ値
と呼ぶんですね!確かによくよく考えてみれば、これらの型が
プリミティブ型
と呼ばれているところから推論することができましたね。笑スッキリしました!
コメント頂きありがとうございました!
頂いた情報をもとに、今夜記事を改善しようと思います!🥳
→ プリミティブ値周りの記事内容を更新しました!🙌(6/2)