はじめに
TypeScriptとReactで開発していて、よく悩むのがフォーム関連のイベントの型です。
今回は型の誤りによりevent.target.valueで値を取得できないという事態に遭遇したので、その理由と対応方法について解説します。
誤った処理の定義
ラジオボタンの値取得処理を以下のように定義していました。
const clickHandler = (event: MouseEvent<HTMLInputElement>) => {
console.log(event.target.value);
};
一見すると合っていそうですが、実際にはVS Code上では以下のようにエラーとなります。

暫定対応
この実装をしていた当時はまったく理由がわからなかったので、代わりに以下のようにして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の型定義を確認します。
interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
target: EventTarget & T;
}
ChangeEventの場合はtargetの型がEventTargetとTの交差型になっています。
よって、ChangeEventはEventTargetとTの両方のプロパティを持ちます。
そして、Tにはジェネリクスに設定した型(ここではHTMLInputElement)が設定されます。
そのため、targetプロパティはvalueをもつのです。
MouseEventの型
こちらもReactの型定義から抜粋します。
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自体が取得できないはずです。
MouseEvemtはUIEventを継承しているので、さらに追ってみます。
なお、ここでのTは先ほどの例においてはHTMLInputElementが設定されます。
UIEvent
interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> {
detail: number;
view: AbstractView;
}
ここにもtargetはありません。
今回の例ではTはUIEventから引き継いでHTMLInputElementです。
また、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
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などのプロパティを持ちません。
ここまで長い旅路でしたが、MouseEventのtargetはプロパティを持たないということがわかりました。
そのためevent.target.valueがエラーとなるのです。
一方、currentTargetは型Cです。
これはSyntheticEventにおいてはEventTargetとTの交差型でした。
そしてSyntheticEventのTはMouseEventでのTと同じであるため、ここではHTMLInputElementとなります。
そのため、event.currentTarget.valueで値が取得できた、ということです。
まとめ
Reactの型定義を深堀りしたので、複雑になりましたが、ちょっとした型の違いで取得できたり、できなかったりする謎が解けました。
TypeScriptでの型は、わずらわしさを感じることも多いですが、適切に設定することで開発効率の向上につながるので、今後も疑問に思ったことは一つずつ紐解いていきます。