LoginSignup
677
483

More than 5 years have passed since last update.

any型で諦めない React.EventCallback

Last updated at Posted at 2019-01-28

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 にマウスオーバーした時のものです。

image.png

答えが全部、型推論に書いてありましたね!これで指定方法が分かったので、コピペします。

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 キーを押しながらマウスクリックすることで、いつもの様に型定義へジャンプします。

@types/react/global.d.ts
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型は一体どこからきているかというと…

image.png

キャプチャ右側のとおり一覧で表示されます。 lib.dom.d.tsglobal.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
}

677
483
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
677
483