React・TypeScriptで書く

props・state・イベントに型を付ける

コンポーネントの型はここから始まります。propsの型の書き方、useStateの推論と明示の使い分け、イベントの型(ChangeEvent・FormEvent)、useRefとnullチェックまで、実際にエラーを消しながら進めます。

前回はエラーの山を作ったところで終わりました。今回から埋めていきます。まずはprops・state・イベントの3つです。

propsの型——type Propsが定番

いちばん多いエラー(Binding element 'bird' implicitly has an 'any' type.)から片づけます。

import type { Bird } from '../types.ts'

type Props = {
  bird: Bird
  isFavorite: boolean
  onToggleFavorite: (id: Bird['id']) => void
}

function BirdCard({ bird, isFavorite, onToggleFavorite }: Props) {

}

読み方はこうです。

  • type Props = { … }——このコンポーネントが受け取るものの一覧
  • : Props——引数がその形であることを宣言
  • (id: Bird['id']) => void——「idを1つ受け取って、何も返さない関数」

3つ目が関数の型です。=>の右が戻り値で、voidは「返さない」という意味。この書き方に慣れると、他人のコードのonXxxが何を渡してくるか、実装を読まなくても分かるようになります。

childrenを受け取るときはReactNode

タグで挟んだ中身を受け取る部品には、ReactNodeという型を使います。

import type { ReactNode } from 'react'

export function BirdsProvider({ children }: { children: ReactNode }) { … }

ReactNodeは「Reactが画面に出せるもの全部」——JSX・文字列・数値・配列・null——をまとめた型です。「何が来るか分からないけど画面に出すもの」には、これを使っておけば間違いありません。

useState——推論に任せる/明示する

useState初期値から型を推論します

const [keyword, setKeyword] = useState('')        // string と推論される
const [onlyFavorites, setOnlyFavorites] = useState(false)  // boolean

この2つに型は書きません。 書いても同じで、読みにくくなるだけです。

問題は空の配列です。

const [birds, setBirds] = useState([])   // ✕ never[] と推論される

空の配列からは「中身が何か」を推論しようがないので、TypeScriptはnever[](何も入れられない配列)と判断します。実際、前回のエラーにこれが出ていました。

error TS2322: Type '{ birds: never[]; … }' is not assignable to type 'null'.
error TS2339: Property 'id' does not exist on type 'never'.

中身の型を教えます。

const [birds, setBirds] = useState<Bird[]>([])   // ○

イベントの型

eventにも型が要ります。使うのはほぼ2種類です。

import type { ChangeEvent, FormEvent } from 'react'

function handleSubmit(event: FormEvent<HTMLFormElement>) { … }

<input onChange={(event: ChangeEvent<HTMLInputElement>) => setName(event.target.value)} />
<select onChange={(event: ChangeEvent<HTMLSelectElement>) => setEmoji(event.target.value)} />

<…>の中はイベントが起きた要素です。HTMLInputElementだとevent.target.valueが使え、HTMLSelectElementなら選択値が取れます。ここを正しく書くと、event.target.まで打った時点で候補が出ます

useRefnull

前回のエラーにこれがありました。

error TS18047: 'nameInputRef.current' is possibly 'null'.

useRef(null)currentは、画面が描かれるまでnullです。TypeScriptはそれを知っているので、いきなり.focus()を呼ぶと止めてくれます。

const nameInputRef = useRef<HTMLInputElement>(null)

nameInputRef.current?.focus()   // ○ nullなら何もしない
  • useRef<HTMLInputElement>(null)——「input要素が入る予定の入れ物」
  • ?.(オプショナルチェーン)——nullのときは呼ばずに素通りする

formsの回で「.currentは最初null」と書きましたが、TypeScriptを入れると、その注意を機械が代わりに見張ってくれます。これが型を入れる効きめのひとつです。

公式ドキュメントも見てみる

ja.react.dev外部サイトTypeScript の使用 – React公式の「TypeScriptの使用」。propsの型・useState・イベントハンドラの書き方が、この回と同じ順で並んでいます

このレッスンのまとめ

  1. propsはtype Props = { … }を作って: Propsで受け取る(interfaceでも可)
  2. 関数のpropsは(id: Bird['id']) => voidの形。=>の右が戻り値
  3. childrenReactNode
  4. useState推論に任せる。空配列・nullなど推論できないときだけ<T>を書く
  5. イベントはChangeEvent<HTMLInputElement>FormEvent<HTMLFormElement>。名前はエディタに聞く
  6. JSXの中のインライン関数は、型を書かなくても推論されることが多い
  7. useRef<HTMLInputElement>(null)currentnullかもしれない——?.で守る

次回はデータそのものの型です。Bird型を1か所に置いて、Contextとカスタムフックにも型を通します。

やってみよう:部品に型を付ける

BirdCardAddBirdFormのエラーを消します。Bird型はまだ無いので、この回では仮にsrc/types.tsを作っておきます(本格的な話は次回)。

  1. src/types.tsを作り、export type Bird = { id: number | string; name: string; emoji: string; area: string; rare: number; memo: string }と書く
  2. BirdCard.tsxtype Propsを足し、{ bird, isFavorite, onToggleFavorite }: Propsにする
  3. AddBirdForm.tsxtype Props = { onAdd: (bird: Bird) => void }を足す
  4. handleSubmitの引数にFormEvent<HTMLFormElement>を付ける
  5. useRef(null)useRef<HTMLInputElement>(null)にして、current?.focus()に直す
  6. npx tsc -bを実行し、この2ファイルのエラーが消えていることを確認する
import { useRef, useState } from 'react'
import type { ChangeEvent, FormEvent } from 'react'
import type { Bird } from '../types.ts'

type Props = {
  onAdd: (bird: Bird) => void
}

function AddBirdForm({ onAdd }: Props) {
  const [name, setName] = useState('')
  const [area, setArea] = useState('')
  const [emoji, setEmoji] = useState('🐧')
  const [error, setError] = useState('')
  const nameInputRef = useRef<HTMLInputElement>(null)

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()

    nameInputRef.current?.focus()
  }

}

エラーは全部は消えません(Contextやページのぶんが残ります)。それでいいので、減っていることだけ確認してください。1ファイルずつ減らしていくのが、実務でも同じ進め方です。

よくある質問

propsの型はtypeとinterfaceどちらで書くべきですか?
どちらでも動きます。Reactのコンポーネントではtypeを使うことが多く、公式のドキュメントもtypeで書かれています。プロジェクトに既存の書き方があれば、それに合わせるのがいちばんです。
useStateにはいつ型を書きますか?
初期値から推論できるときは書きません。useState('')はstringになります。書くのは推論できないとき、たとえばuseState([])だと空配列としか分からないので、useState<Bird[]>([])のように中身の型を伝えます。
イベントの型が長くて覚えられません
覚えなくて大丈夫です。onChange={(event) => …}と書いてeventにマウスを乗せると、エディタが正しい型を教えてくれます。それをコピーするのが実務でも普通のやり方です。
useRefでnullのエラーが出ます
useRef(null)のcurrentは最初nullなので、TypeScriptがnullの可能性を指摘します。current?.focus()のようにオプショナルチェーンを使うか、存在を確認してから使ってください。