イベント処理の型を覚えるのは大変
フォームやインプットなどのイベントハンドラに渡すイベント処理関数に対する型は、
FormEventHandler<HTMLFormElement>
ChangeEventHandler<HTMLInputElement>
といった型のエイリアスを呼びだして定義するか、
(event: FormEvent<HTMLFormElement>) => void
(event: ChangeEvent<HTMLInputElement>) => void
のように型のエイリアスと組み合わせて定義します。
このほかにもエイリアスが存在するため、全てを覚えるのは大変です。
仮に一字一句覚えたとしても誤って不適切な型を定義するとエラーの元になります。
本記事では、VS Codeの機能を活用して適切な型の調査方法と定義方法について紹介します。
また応用編として、Material-UIの定義方法について紹介します。
前提
- VS Codeは2021/9/15時点の最新版
- Reactプロジェクトのモジュール構成は次の通り(Node.jsはv15.9.0)
{
"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
を設定して、厳格に型をチェックしています。
そもそも型を定義しないと何が起こるのか?
簡単なフォームとイベント処理関数を型定義しないで実装したとします。
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
の上にマウスカーソルをホバーすると見ることができます。)
また、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型の定義があると許してくれません。厳しい…
このエラーを回避するにはイベント処理関数に型を定義するか、次のようにイベントハンドラに直接コールバックを書く必要があります。
<form onSubmit={(event) => {console.log(event);}}>
この状態でevent
にマウスカーソルをホバーすると、event
に対してFormEventHandler<HTMLFormElement>
の型が自動的に定義されていることがわかります。便利!
ただし、リファクタリングでコールバック関数の部分を抜き出して、イベント処理関数として定義しようとすると再びエラーが発生するため、イベント処理関数への型定義は避けて通れません。
ではどうするのか?
型の調査方法
実は、前章でヒントが書かれています。
それは
マウスカーソルをホバー
です!!
formタグのonSubmit
とinputタグのonChange
にそれぞれマウスカーソルをホバーすると、VS Codeの機能として次のヒントが表示されます。
formonSubmit
inputonChange
ここで重要になるのが、onSubmitやonChangeに定義されている型です。
一部省略しますが、次のように型が定義されています。
(省略).onSubmit?: React.FormEventHandler<HTMLFormElement> | undefined
(省略).onChange?: React.ChangeEventHandler<HTMLInputElement> | undefined
React.FormEventHandle
やReact.ChangeEventHandler
は'react'モジュールで用意されている型のエイリアスです。
これは、冒頭に述べた型のエイリアスそのもです。
FormEventHandler<HTMLFormElement>
ChangeEventHandler<HTMLInputElement>
型の定義方法
これらの型のエイリアスを使ってイベント処理関数に対して型を定義することで、イベントハンドラに直接コールバックを書いたときのようにevent
に対して適切な型が自動的に設定されるようになります。
まず、ホバーした際に表示されるヒントの上にマウスカーソルを移動して、エイリアスの部分をドラッグ&ドロップすると文字列が選択できます。
これをコピーして、イベント処理関数を定義している変数に対してエイリアスを貼り付けて型を定義します。これだけ。
作業の様子は次のようになります。
型を定義した瞬間にhandleOnSubmit関数の引数event
のエラー表示(赤波線)が消えます。
このevent
にマウスカーソルをホバーすると、自動的に型定義されていることが確認できます。
これは、formタグのイベントハンドラにコールバックを設定したときと同じ型が定義されています。
handleOnChange
も同様の手順で型を定義してコードを整理すると、次のようになります。
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'モジュールに格納されているため、次のように呼び出して使うことができます。
import { ChangeEventHandler, FormEventHandler, memo, VFC } from 'react';
ここでFormEventHandler
とChangeEventHandler
について簡単に説明します。
FormEventHandler
とChangeEventHandler
の正体
これらもマウスカーソルをホバーすることでヒントを見ることができます。
FormEventHandler
ChangeEventHandler
それぞれ右辺を抽出すると、次のようになります。
(event: FormEvent<T>) => void
(event: ChangeEvent<T>) => void
ここで<T>
はジェネリクスなので、<HTMLFormElement>
と<HTMLInputElement>
がそれぞれ代入されて、次のようになります。
(event: FormEvent<HTMLFormElement>) => void
(event: ChangeEvent<HTMLInputElement>) => void
つまりFormEventHandler
とChangeEventHandler
は戻り値無しの関数の型がエイリアスとして定義されていたわけです。
型定義の恩恵
ここまで、型の調査方法と定義方法について説明しました。
JavaScript( jsx )で書くよりも手間が掛かるため面倒ですが、型を定義することで手間に見合った恩恵を受けることができます。
それぞれのイベント処理に入れたconsole.log(event)
に.
を入力してみてください。
すると次のようにインテリセンスが働きます。これが一つ目の恩恵です。
JavaScript( jsx )で同じことをやろうとすると、上記の候補は一つも出ません。
出てくるの使ったことがある語句程度です。
次に、それぞれのイベント処理関数に入れたconsole.log(event)
をconsole.log(event.target.value)
に変更します。
するとhandleOnSubmit
側のvalue
にエラーが発生します。
value
にマウスカーソルをホバーすると、エラーメッセージが次のように表示されます。
これは、formのevent.target
にはそもそもvalue
のプロパティが存在しないからです。
(formの場合、event.currentTargetからFormDataに投げるなどして加工する必要があります)
このエラー表示が二つ目の恩恵です。
応用編:Material-UIの定義方法
ここまではformやinputなどのhtmlタグに対しての説明でした。
Reactには様々なUIフレームワークが存在します。
これらを使う場合もしっかりと型を定義する必要があります。
本章では応用編としてUIフレームワークの一つ、Material-UIの定義方法を説明します。
ソースコード
最初と同様に簡単なインプットフォームと、イベント処理関数を定義したとします。
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;
この中で、フォームに関する記述は次のようになります。
<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
にマウスカーソルをホバーすると、次のようになります。
BoxonSubmit
TextFieldonChange
BoxのonSubmit
はコピーアンドペーストで何とかなりそうですが、TextFieldのonChange
は特にエイリアスで定義されているわけではなく、onChange:(event:any)=>void
となっています。
これはonSubmit
に対して3つの関数が定義されていることに起因しています。
command(windowsはctrl)を押しながらクリックすると、次のように展開されます。
これはイベントハンドラに入れるモノによって決まります。
今回の場合、型が未定義のイベント処理関数を入れたことで、3つのうち1つの関数が確定して最終的に(event:any)=>void
が表示されています。
しかし、これではダメです。
では、どうするのか?
答えは、コールバックを入れるです。
コールバックを入れた状態でマウスカーソルをホバーすると、次のようになります。
型を定義
これらの情報をもとに型を定義すると、次のようになります。
const handleOnSubmit:FormEventHandler<HTMLFormElement> = (event) => {
console.log(event);
};
const handleOnChange:(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void = (event) => {
console.log(event);
};
まとめ
イベント処理関数に適切な型を「調査」して「定義」する方法と応用方法について説明しました。
まとめると、型調査はマウスカーソルをホバーで型のエイリアスまたは関数の型を表示して、この情報をもとに型を定義する流れとなります。
しかし、イベントハンドラに複数の関数が定義されている場合、型調査で(event:any)=>void
と表示されることがあります。
その場合は、イベントハンドラにコールバック関数を入れて変化を確認してみてください。