2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

TypeScriptとReactで開発していて、よく悩むのがフォーム関連のイベントの型です。

今回は型の誤りによりevent.target.valueで値を取得できないという事態に遭遇したので、その理由と対応方法について解説します。

誤った処理の定義

ラジオボタンの値取得処理を以下のように定義していました。

  const clickHandler = (event: MouseEvent<HTMLInputElement>) => {
    console.log(event.target.value);
  };

一見すると合っていそうですが、実際にはVS Code上では以下のようにエラーとなります。
image.png

暫定対応

この実装をしていた当時はまったく理由がわからなかったので、代わりに以下のようにしてvalueを取得しました。

  const clickHandler = (event: MouseEvent<HTMLInputElement>) => {
    // console.log(event.target.value);
    console.log(event.currentTarget.value);
  };

一応解決はしましたが、currentTargetプロパティからはvalueが取得できるのに、targetプロパティからは取得できないという不可解な事態にもやもやしていました。

正しい処理の定義

そもそも、ラジオボタンの値取得方法が誤っていました。
onClickに設定するためのクリックイベント取得処理を書いていましたが、本来はonChangeに値変更時の処理を記載するべきです。

  const changeHandler = (event: ChangeEvent<HTMLInputElement>) => {
    console.log(event.target.value);
  };

このようにすると、targetプロパティからvalueを取得できます。
先ほどとどこが違うかわかるでしょうか。

誤った処理ではクリックイベントを取得する想定だったので、イベントの型が
MouseEvent<HTMLInputElement>
となっています。

一方、こちらの処理ではイベントの型が
ChangeEvent<HTMLInputElement>
となっています。

このように、設定する型によって同じプロパティでも取得の可否が分かれてきます。

何が違うのか

これで一件落着とおもいきや、実はひとつ引っかかることがあります。

実はvalueプロパティは、HTMLInputElementに属するプロパティです。

つまり、MouseEvent<HTMLInputElement>だろうが、ChangeEvent<HTMLInputElement>だろうが、ジェネリクスにHTMLInputElementを設定している以上、どちらからもvalueが取得できそうです。

ChangeEventの型

Reactの型定義を確認します。

ChangeEventの型定義
interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
  target: EventTarget & T;
}

ChangeEventの場合はtargetの型がEventTargetTの交差型になっています。
よって、ChangeEventEventTargetTの両方のプロパティを持ちます。

そして、Tにはジェネリクスに設定した型(ここではHTMLInputElement)が設定されます。
そのため、targetプロパティはvalueをもつのです。

MouseEventの型

こちらもReactの型定義から抜粋します。

MouseEventの型定義
interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> {
  altKey: boolean;
  button: number;
  buttons: number;
  clientX: number;
  clientY: number;
  ctrlKey: boolean;
  /**
   * See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method.
   */
  getModifierState(key: ModifierKey): boolean;
  metaKey: boolean;
  movementX: number;
  movementY: number;
  pageX: number;
  pageY: number;
  relatedTarget: EventTarget | null;
  screenX: number;
  screenY: number;
  shiftKey: boolean;
}

確認するとわかりますが、そもそもtargetプロパティがありません。
そうなると、そもそもMouseEventからはtarget自体が取得できないはずです。

MouseEvemtUIEventを継承しているので、さらに追ってみます。
なお、ここでのTは先ほどの例においてはHTMLInputElementが設定されます。

UIEvent

UIEventの型定義
interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> {
  detail: number;
  view: AbstractView;
}

ここにもtargetはありません。
今回の例ではTUIEventから引き継いでHTMLInputElementです。
また、SyntheticEventを継承しているようなので、さらに深く追っていきます。

SyntheticEvent

SyntheticEventの型定義
interface SyntheticEvent<T = Element, E = Event>
  extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {}

何も定義されていません。
BaseSyntheticEventを継承しているので、もう一段深く確認します。
なお、UIEventから引き継いだT型はBaseSyntheticEventの2番目のジェネリクスに渡されます。
3番目の型はEventTargetです。
EventTargetは次のように、空の定義となっています。

interface EventTarget {}

BaseSyntheticEvent

BaseSyntheticEventの型定義
interface BaseSyntheticEvent<E = object, C = any, T = any> {
  nativeEvent: E;
  currentTarget: C;
  target: T;
  bubbles: boolean;
  cancelable: boolean;
  defaultPrevented: boolean;
  eventPhase: number;
  isTrusted: boolean;
  preventDefault(): void;
  isDefaultPrevented(): boolean;
  stopPropagation(): void;
  isPropagationStopped(): boolean;
  persist(): void;
  timeStamp: number;
  type: string;
}

ようやくtargetが出てきました。

targetの型Tはジェネリクスの3番目の型になっています。
3番目の型はEventTargetという空の定義でした。
つまり、valueなどのプロパティを持ちません。

ここまで長い旅路でしたが、MouseEventtargetはプロパティを持たないということがわかりました。

そのためevent.target.valueがエラーとなるのです。

一方、currentTargetは型Cです。
これはSyntheticEventにおいてはEventTargetTの交差型でした。
そしてSyntheticEventTMouseEventでのTと同じであるため、ここではHTMLInputElementとなります。

そのため、event.currentTarget.valueで値が取得できた、ということです。

まとめ

Reactの型定義を深堀りしたので、複雑になりましたが、ちょっとした型の違いで取得できたり、できなかったりする謎が解けました。

TypeScriptでの型は、わずらわしさを感じることも多いですが、適切に設定することで開発効率の向上につながるので、今後も疑問に思ったことは一つずつ紐解いていきます。

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?