🎒

【初心者】React × TypeScript 基本の型定義

2021/05/23に公開
2

はじめに

ここ最近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)が何かわかる
  • onClickonChangeなどのイベントハンドラの使い方がわかる

この記事が求める「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という型を用います。
VFCVoid 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パターンがあります。

propsなしの型定義
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>として型を指定するパターン

どちらのパターンで書いても結果は変わりません。

propsありの型定義
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>のデフォルト値として{}が指定されています。

node_modules/@types/react/index.d.ts
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ではonClickonChangeなどのイベントハンドラを用いて、イベントオブジェクト(event)を扱うことも多くあるかと思います。
最後に、イベントハンドラやイベントオブジェクトの型定義の方法について見ていきます。

イベントハンドラ / イベントオブジェクトの型定義

onClickonChangeなどのイベントハンドラ、そしてイベントオブジェクト(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の問題なのか、onClickonChangeにマウスを乗せてみても、記事の画像とは異なりイベントオブジェクトの型情報は表示されませんでした。


参考記事の通りにマウスを乗せると自分の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の型定義(プリミティブ値)」の部分の内容を更新しました。
コメントして頂きありがとうございました!🙇‍♀️

参考

GitHubで編集を提案
マナリンク Tech Blog

Discussion

morimorimorimori

コメント失礼します。
綺麗にまとめられており、理解しやすかったです!このテーマだと膨大な量になりがちですが、要点を抑えてコンパクトにまとめるのが上手ですね。続きがあればぜひ読んでみたいです!

「1つの値」というのを言葉にするのが中々難しいのですが、ステートに保持する値が配列やオブジェクトではない場合のことを指しています。
ステートが「なにか1つの値(文字列とか数値とか真偽値とか)」である時の場合です(わかりやすい呼び方を募集しています😓)。

1点お節介ですが、こちらプリミティブ値のことではないかな?と思いました!

koukou

morimorig3さん、コメントありがとうございます!

綺麗にまとめられており、理解しやすかったです!このテーマだと膨大な量になりがちですが、要点を抑えてコンパクトにまとめるのが上手ですね。続きがあればぜひ読んでみたいです!

ありがとうございます!こういった感想を頂けるのが何より嬉しいです☺️

1点お節介ですが、こちらプリミティブ値のことではないかな?と思いました!

お節介なんてとんでもないです!むしろ待ちわびていました笑
なるほど、 プリミティブ値 と呼ぶんですね!
確かによくよく考えてみれば、これらの型が プリミティブ型 と呼ばれているところから推論することができましたね。笑
スッキリしました!

コメント頂きありがとうございました!
頂いた情報をもとに、今夜記事を改善しようと思います!🥳

→ プリミティブ値周りの記事内容を更新しました!🙌(6/2)