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