React x TypeScript の鬼門のひとつに「props に記述する EventCallback の適切な書き方が分からない」というものがあります。さて、このコンポーネントの type Props
どう型定義するべきでしょうか?
const View: React.FC<Props> = props => (
<form onSubmit={props.onSubmit}>
<input
type="text"
onClick={props.onClick}
onChange={props.onChange}
onKeyPress={props.onkeypress}
onBlur={props.onBlur}
onFocus={props.onFocus}
/>
<div onClick={props.onClickDiv} />
</form>
)
「とくに困らないので〜」という理由でつぎの様に妥協していませんか? 部分的妥協は、TypeScript の良い点でもありますが、せっかくなのできちんとしたいですよね。
type Props = {
onClick: (event: any) => void
onChange: (event: any) => void
onkeypress: (event: any) => void
onBlur: (event: any) => void
onFocus: (event: any) => void
onSubmit: (event: any) => void
onClickDiv: (event: any) => void
}
ただ、EventCallback の型はたくさんあるし、初見ではどれを指定すれば良いのか…迷いますよね。自分は普段、VSCode を使っているため、迷ったときは VSCode のコードヒントに頼ることにしています。つぎのキャプチャは、onClick まで空で書いて、 onClick にマウスオーバーした時のものです。
答えが全部、型推論に書いてありましたね!これで指定方法が分かったので、コピペします。
type Props = {
onClick: (event: React.MouseEvent<HTMLInputElement>) => void
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
onkeypress: (event: React.KeyboardEvent<HTMLInputElement>) => void
onBlur: (event: React.FocusEvent<HTMLInputElement>) => void
onFocus: (event: React.FocusEvent<HTMLInputElement>) => void
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
onClickDiv: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
}
Generics に指定している型はなに?
さて、React.MouseEvent<XXX>
やReact.ChangeEvent<XXX>
の様に、Generics にHTMLInputElement
型や HTMLDivElement
型など、いろいろな型が指定されていますね…。
import もしていないし、この型はどこから来たもので、いったいこれらの指定で何が変わるのでしょうか? HTMLInputElement
型の中身をみてみましょう。型にマウスを当て、Option キーを押しながらマウスクリックすることで、いつもの様に型定義へジャンプします。
interface HTMLDivElement extends HTMLElement { }
interface HTMLDListElement extends HTMLElement { }
interface HTMLEmbedElement extends HTMLElement { }
interface HTMLFieldSetElement extends HTMLElement { }
interface HTMLFormElement extends HTMLElement { }
interface HTMLHeadingElement extends HTMLElement { }
interface HTMLHeadElement extends HTMLElement { }
interface HTMLHRElement extends HTMLElement { }
interface HTMLHtmlElement extends HTMLElement { }
interface HTMLIFrameElement extends HTMLElement { }
interface HTMLImageElement extends HTMLElement { }
interface HTMLInputElement extends HTMLElement { }
interface HTMLModElement extends HTMLElement { }
interface HTMLLabelElement extends HTMLElement { }
interface HTMLLegendElement extends HTMLElement { }
interface HTMLLIElement extends HTMLElement { }
紙面の都合上省略していますが、こんなにあるのか…とたくさん出てきましたね!よくみてみると…。ほとんどが extends HTMLElement { }
ですね。名前が違うだけで、全部一緒じゃないですかw でも実は、interface にはオーバーロードの仕組みがあり、それぞれの型は、@types/react/global.d.ts
で明示されていない定義を上書きしています。元の HTMLInputElement
型は一体どこからきているかというと…
キャプチャ右側のとおり一覧で表示されます。 lib.dom.d.ts
と global.d.ts
で定義されていることが分かりますね。global.d.ts
は、@types/react で提供されている定義、lib.dom.d.ts
は TypeScript から提供されている定義です。
「lib.dom
なんて入れた覚えが無いよー」と不思議に思うかもしれませんが、これの出どころは、tsconfig の lib 指定です。tsconfig の lib 指定が無い場合、他の指定内容(targetなど)に応じて適切なデフォルト lib が import されるためこの様になります。出所不明だった HTMLInputElement
型 はこの様に、interface のオーバーロードの仕組みで塗り重ねられていたんですね。
必要に応じて、抽象度を見極める
onClick
を Divタグ・Buttonタグなど、複数に適用したい場合、詳細な型である HTMLInputElement
型や HTMLDivElement
型 を指定してしまうと、汎用的でなく不便な場合があります。そういう時は、継承元の HTMLElement
型など、抽象的な型を指定すれば良いです。
type Props = {
onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void
}
EventCallback の引数型をしっかり享受したい場合は、型推論通りの内容を使いましょう。
type Props = {
onClickButton: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
}