React+TypeScript+VS Codeでイベント処理関数に適切な型を「調査」して「定義」する方法

## イベント処理の型を覚えるのは大変
フォームやインプットなどのイベントハンドラに渡すイベント処理関数に対する型は、

```tsx
FormEventHandler<HTMLFormElement>
ChangeEventHandler<HTMLInputElement>
```

といった型のエイリアスを呼びだして定義するか、

```jsx
(event: FormEvent<HTMLFormElement>) => void
(event: ChangeEvent<HTMLInputElement>) => void
```

のように型のエイリアスと組み合わせて定義します。

このほかにもエイリアスが存在するため、全てを覚えるのは大変です。
仮に一字一句覚えたとしても誤って不適切な型を定義するとエラーの元になります。

本記事では、VS Codeの機能を活用して適切な型の**調査方法**と**定義方法**について紹介します。
また応用編として、**Material-UIの定義方法**について紹介します。

## 前提

- VS Codeは2021/9/15時点の最新版
- Reactプロジェクトのモジュール構成は次の通り(Node.jsはv15.9.0)

```json:package.json(一部抜粋)
{
  "dependencies": {
    "@emotion/react": "^11.4.1",
    "@emotion/styled": "^11.3.0",
    "@mui/icons-material": "^5.0.0-rc.0",
    "@mui/material": "^5.0.0-rc.0",
    "@mui/styled-engine": "^5.0.0-rc.0",
    "@testing-library/jest-dom": "^5.14.1",
    "@testing-library/react": "^12.1.0",
    "@testing-library/user-event": "^13.2.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "4.0.3"
  },
  "devDependencies": {
    "@types/jest": "^27.0.1",
    "@types/node": "^16.7.10",
    "@types/react": "^17.0.19",
    "@types/react-dom": "^17.0.9",
    "@types/react-router-dom": "^5.1.8",
    "typescript": "^4.4.3"
  },
}
```

- **tsconfig.json**は`"strict": true`を設定して、厳格に型をチェックしています。

## そもそも型を定義しないと何が起こるのか？
簡単なフォームとイベント処理関数を型定義しないで実装したとします。

```tsx:App.tsx
import { memo, VFC } from 'react';

const App: VFC = memo(() => {
  // formタグのイベント処理
  const handleOnSubmit = (event) => {
    console.log(event);
  };

  // inputタグのイベント処理
  const handleOnChange = (event) => {
    console.log(event);
  };

  return (
    <form onSubmit={handleOnSubmit}>
      <input type="text" name="text" onChange={handleOnChange} />
      <input type="submit" value="submit" />
    </form>
  );
});

export default App;
```

このときのVS Codeでは、各イベント処理の`event`に対して次のエラーが出ます。
(`event`の上に**マウスカーソルをホバー**すると見ることができます。)
![スクリーンショット 2021-09-16 13.46.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/887d6b68-893e-9f38-66e1-b339e37e650e.png)
また、`yarn start`で実行すると、VS Codeのコンソールと表示用のブラウザに次のエラーが表示されます。

```
/(中略)/App.tsx
TypeScript error in /(中略)/App.tsx(5,27):
Parameter 'event' implicitly has an 'any' type.  TS7006

    3 | const App: VFC = memo(() => {
    4 |   // formタグのイベント処理
  > 5 |   const handleOnSubmit = (event) => {
      |                           ^
    6 |     console.log(event);
    7 |   };
    8 |
```

これは`event`に対して暗黙的にany型が定義されていることが原因です。
jsxで保存して`yarn start`で実行すると正常動きますが、TypeScriptではこのような暗黙的なany型の定義があると許してくれません。厳しい…

このエラーを回避するには**イベント処理関数に型を定義**するか、次のように**イベントハンドラに直接コールバックを書く**必要があります。

```tsx
    <form onSubmit={(event) => {console.log(event);}}>
```

この状態で`event`に**マウスカーソルをホバー**すると、`event`に対して`FormEventHandler<HTMLFormElement>`の型が自動的に定義されていることがわかります。便利！
![スクリーンショット 2021-09-16 14.22.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/74a467b4-f694-4e34-f9da-5c8daf230281.png)
ただし、リファクタリングでコールバック関数の部分を抜き出して、イベント処理関数として定義しようとすると再びエラーが発生するため、**イベント処理関数への型定義**は避けて通れません。

ではどうするのか？

## 型の調査方法
実は、前章でヒントが書かれています。

それは

**マウスカーソルをホバー**

です！!

formタグの`onSubmit`とinputタグの`onChange`にそれぞれ**マウスカーソルをホバー**すると、VS Codeの機能として次のヒントが表示されます。

form`onSubmit`
![スクリーンショット 2021-09-16 15.00.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/8ddd7a8a-ee43-41ce-0753-fb5105a12419.png)
input`onChange`
![スクリーンショット 2021-09-16 15.00.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/a128fea4-a387-07e1-5c89-6ac4054f6196.png)
ここで重要になるのが、onSubmitやonChangeに定義されている型です。
一部省略しますが、次のように型が定義されています。

```tsx
(省略).onSubmit?: React.FormEventHandler<HTMLFormElement> | undefined
(省略).onChange?: React.ChangeEventHandler<HTMLInputElement> | undefined
```

`React.FormEventHandle`や`React.ChangeEventHandler`は'react'モジュールで用意されている型のエイリアスです。
これは、冒頭に述べた型のエイリアスそのもです。

```tsx:冒頭のエイリアス(再掲)
FormEventHandler<HTMLFormElement>
ChangeEventHandler<HTMLInputElement>
```

## 型の定義方法
これらの型のエイリアスを使ってイベント処理関数に対して型を定義することで、イベントハンドラに直接コールバックを書いたときのように`event`に対して適切な型が自動的に設定されるようになります。

まず、ホバーした際に表示されるヒントの上にマウスカーソルを移動して、エイリアスの部分をドラッグ＆ドロップすると文字列が選択できます。
これをコピーして、イベント処理関数を定義している変数に対してエイリアスを貼り付けて型を定義します。これだけ。

作業の様子は次のようになります。
![ezgif-6-00cbce31d197.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/caf1b25f-c939-5d45-64e1-59d393050431.gif)
型を定義した瞬間にhandleOnSubmit関数の引数`event`のエラー表示(赤波線)が消えます。
この`event`に**マウスカーソルをホバー**すると、自動的に型定義されていることが確認できます。
![スクリーンショット 2021-09-16 16.35.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/3af1100d-aa1b-d343-cd0c-b17868cb6f20.png)
これは、formタグのイベントハンドラにコールバックを設定したときと同じ型が定義されています。
![スクリーンショット 2021-09-16 14.22.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/74a467b4-f694-4e34-f9da-5c8daf230281.png)

`handleOnChange`も同様の手順で型を定義してコードを整理すると、次のようになります。

```tsx:App.tsx
import { ChangeEventHandler, FormEventHandler, memo, VFC } from 'react';

const App: VFC = memo(() => {
  // formタグのイベント処理
  const handleOnSubmit: FormEventHandler<HTMLFormElement> = (event) => {
    console.log(event);
  };

  // inputタグのイベント処理
  const handleOnChange: ChangeEventHandler<HTMLInputElement> = (event) => {
    console.log(event);
  };

  return (
    <form onSubmit={handleOnSubmit}>
      <input type="text" name="text" onChange={handleOnChange} />
      <input type="submit" value="submit" />
    </form>
  );
});

export default App;
```

型のエイリアス`FormEventHandler`と`ChangeEventHandler`は、'react'モジュールに格納されているため、次のように呼び出して使うことができます。

```tsx
import { ChangeEventHandler, FormEventHandler, memo, VFC } from 'react';
```
ここで`FormEventHandler`と`ChangeEventHandler`について簡単に説明します。

### `FormEventHandler`と`ChangeEventHandler`の正体
これらも**マウスカーソルをホバー**することでヒントを見ることができます。
`FormEventHandler`
![スクリーンショット 2021-09-16 17.22.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/23f4c098-08f5-28b2-9b89-953166834381.png)
`ChangeEventHandler`
![スクリーンショット 2021-09-16 17.07.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/6cd17312-9cec-05b5-593f-531a3518ee8e.png)
それぞれ右辺を抽出すると、次のようになります。

```tsx
(event: FormEvent<T>) => void
(event: ChangeEvent<T>) => void
```

ここで`<T>`はジェネリクスなので、`<HTMLFormElement>`と`<HTMLInputElement>`がそれぞれ代入されて、次のようになります。

```tsx
(event: FormEvent<HTMLFormElement>) => void
(event: ChangeEvent<HTMLInputElement>) => void
```

つまり`FormEventHandler`と`ChangeEventHandler`は戻り値無しの関数の型がエイリアスとして定義されていたわけです。

## 型定義の恩恵

ここまで、型の調査方法と定義方法について説明しました。
JavaScript( jsx )で書くよりも手間が掛かるため面倒ですが、型を定義することで手間に見合った恩恵を受けることができます。

それぞれのイベント処理に入れた`console.log(event)`に`.`を入力してみてください。
すると次のようにインテリセンスが働きます。これが一つ目の恩恵です。
![スクリーンショット 2021-09-16 17.57.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/6d3ae550-d0a2-f9e3-e0d0-6956b31a2f99.png)
![スクリーンショット 2021-09-16 17.56.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/96b3e9ea-90e6-2a19-5f04-acc924435e15.png)
JavaScript( jsx )で同じことをやろうとすると、上記の候補は一つも出ません。
出てくるの使ったことがある語句程度です。
![スクリーンショット 2021-09-16 22.56.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/7fc65d31-8000-af4c-e8bb-52099345f95b.png)

次に、それぞれのイベント処理関数に入れた`console.log(event)`を`console.log(event.target.value)`に変更します。
すると`handleOnSubmit`側の`value`にエラーが発生します。
`value`に**マウスカーソルをホバー**すると、エラーメッセージが次のように表示されます。
![スクリーンショット 2021-09-16 23.02.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/b1224e1e-30fc-8bab-ac3f-c1ccba3a165b.png)
これは、formの`event.target`にはそもそも`value`のプロパティが存在しないからです。
(formの場合、event.currentTargetからFormDataに投げるなどして加工する必要があります)
このエラー表示が二つ目の恩恵です。

## 応用編：Material-UIの定義方法
ここまではformやinputなどのhtmlタグに対しての説明でした。
Reactには様々なUIフレームワークが存在します。
これらを使う場合もしっかりと型を定義する必要があります。
本章では応用編としてUIフレームワークの一つ、Material-UIの定義方法を説明します。

### ソースコード
最初と同様に簡単なインプットフォームと、イベント処理関数を定義したとします。

```tsx:SignIn.tsx
import { memo, VFC } from 'react';
import { Box, Button, Container, createTheme, CssBaseline, TextField, ThemeProvider } from '@mui/material';

const theme = createTheme();

const SignIn: VFC = memo(() => {
  const handleOnSubmit = (event) => {
    console.log(event);
  };

  const handleOnChange = (event) => {
    console.log(event);
  };

  return (
    <ThemeProvider theme={theme}>
      <Container component="main" maxWidth="xs">
        <CssBaseline />
        <Box sx={{marginTop: 8, display: 'flex', flexDirection: 'column', alignItems: 'center'}} >
          <Box component="form" onSubmit={handleOnSubmit} noValidate sx={{ mt: 1 }}>
            <TextField margin="normal" required fullWidth id="user" label="ニックネーム" name="user" autoFocus onChange={handleOnChange} />
            <Button type="submit" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}>
              はじめる
            </Button>
          </Box>
        </Box>
      </Container>
    </ThemeProvider>
  );

});
export default SignIn;
```

この中で、フォームに関する記述は次のようになります。

```tsx:フォームのみを抜粋
<Box component="form" onSubmit={handleOnSubmit} noValidate sx={{ mt: 1 }}>
  <TextField margin="normal" required fullWidth id="user" label="ニックネーム" name="user" autoFocus onChange={handleOnChange} />
  <Button type="submit" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}>
    はじめる
  </Button>
</Box>
```
### 型の調査
Boxの`onSubmit`とTextFieldの`onChange`に**マウスカーソルをホバー**すると、次のようになります。
Box`onSubmit`
![スクリーンショット 2021-09-17 0.17.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/a0f33bcf-0294-cd07-f7b9-37b2ab3c90ff.png)
TextField`onChange`![スクリーンショット 2021-09-17 0.33.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/fa3ddc9c-fdbc-7b16-38bb-3b69d37c9445.png)
Boxの`onSubmit`はコピーアンドペーストで何とかなりそうですが、TextFieldの`onChange`は特にエイリアスで定義されているわけではなく、`onChange:(event:any)=>void`となっています。
これは`onSubmit`に対して3つの関数が定義されていることに起因しています。
command(windowsはctrl)を押しながらクリックすると、次のように展開されます。
![スクリーンショット 2021-09-17 0.42.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/3911d494-52d5-a368-5593-36665e92536c.png)
これはイベントハンドラに入れるモノによって決まります。
今回の場合、型が未定義のイベント処理関数を入れたことで、3つのうち1つの関数が確定して最終的に`(event:any)=>void`が表示されています。
しかし、これではダメです。

では、どうするのか？

答えは、**コールバックを入れる**です。
コールバックを入れた状態で**マウスカーソルをホバー**すると、次のようになります。
![スクリーンショット 2021-09-17 1.01.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/890567/1d75ca41-1fc2-06ff-51e9-33dbf4e45134.png)
### 型を定義
これらの情報をもとに型を定義すると、次のようになります。

```tsx
  const handleOnSubmit:FormEventHandler<HTMLFormElement> = (event) => {
    console.log(event);
  };

  const handleOnChange:(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void = (event) => {
    console.log(event);
  };
```

## まとめ
イベント処理関数に適切な型を「調査」して「定義」する方法と応用方法について説明しました。

まとめると、型調査は**マウスカーソルをホバー**で型のエイリアスまたは関数の型を表示して、この情報をもとに型を定義する流れとなります。
しかし、イベントハンドラに複数の関数が定義されている場合、型調査で`(event:any)=>void`と表示されることがあります。
その場合は、イベントハンドラにコールバック関数を入れて変化を確認してみてください。