【TypeScript】Reactでのイベントに設定する型の理解が少しだけ深まる
Reactに限らずインタラクティブなUIを持つアプリケーションを構築する場合必ずイベントを設定する必要があります。TypeScriptを学習し始めた人の中にはイベントの型は複雑そうで何をしているのかよくわからないが言われるがままに設定したら動作したので気にしないという人は多いのではないでしょうか。
本文書ではイベントの型がよくわからないまま設定したら動作しているけど本当は理解したい人を対象に少しだけイベントの型の理解が深まるようにbutton要素のonClickイベントとinput要素のonChangeイベントを利用して説明を行なっています。
動作確認を行ったReactのバージョンは18.2.0でcrate-react-appにTypeScriptオプションをつけてプロジェクトを作成しています。エディターにはVisual Sutdio Codeを(VSCode)利用してTypeScriptの入力支援の力を借りています。
目次
Button要素からのイベント
利用するコード
利用するコードはbutton要素にonClickイベントを設定してボタンをクリックするとhandleClick関数を実行するシンプルなコードから始めていきます。
import './App.css';
function App() {
const handleClick = (event) => {
console.log(event);
};
return (
<div className="App">
<button onClick={handleClick}>Click</button>
</div>
);
}
export default App;
JavaScript環境であれば上記のコードは問題なく実行できますがTypeScript環境ではnpm start/yarn startコマンドを実行し開発サーバを起動するとブラウザの画面上に以下のエラーが表示されます。
Compiled with problems:X
ERROR in src/App.tsx:4:24
TS7006: Parameter 'event' implicitly has an 'any' type.
引数のeventに型が設定されていないことが原因なので型にanyを設定することでエラーは解消されます。
const handleClick = (event: any) => {
console.log(e);
};
エラーが解消するとブラウザのデベロッパーツールのコンソールでeventオブジェクトの中身を確認することができます。eventオブジェクトの中身を確認したのには意味がありeventオブジェクトを構成するプロパティとこれから確認するイベントの型の設定と一致するか確認する時に利用します。
MouseEventの設定
エラーを解消するためだけに先程は型にanyを設定していましたが正しい型は何を設定すればいいのでしょう。TypeScriptの推論とVSCodeの力を借りて型を確認します。onClickの中身からhandleClickを削除してインラインで関数を記述し引数にはeventと記述します。
import './App.css';
function App() {
const handleClick = (event: any) => {
console.log(event);
};
return (
<div className="App">
<button onClick={(event)=>{}}>Click</button>
</div>
);
}
export default App;
eventにカーソルと当てるとeventの型はReact.MouseEvent<HTMLButtonElement, MouseEvent>であることがわかります。
先ほど設定したeventの型をanyからReact.MouseEvent<HTMLButtonElement, MouseEvent>に変更します。これでeventに対する型の設定は完了です。
import React from 'react';
import './App.css';
function App() {
const handleClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
console.log(event);
};
return (
<div className="App">
<button onClick={handleClick}>Click</button>
</div>
);
}
export default App;
なぜこの型を設定すれば動作するのかわからないけど動いたので問題ないかと深く調べない人も多いかと思いますが本文書ではさらに型の設定について確認していきます。
2つのMouseEvent
下記のコードのようにreactからMouseEventを直接importするコードに変更するとエラーが発生します。MouseEventを利用している場所にはeventの型のMouseEventとそのMouseEventのジェネリクスに利用しているMouseEventの2つがあります。MouseEventの型定義を理解していない場合は何が原因かわからないと思います。
import { MouseEvent } from 'react';
import './App.css';
function App() {
const handleClick = (event: MouseEvent<HTMLButtonElement, MouseEvent>) => {
console.log(event);
};
//略
MouseEventの型定義を確認するためにMouseEventにカーソルを合わせてCmdとクリックします(VSCodeの場合)。型定義が表示され、MouseEventの型定義はindex.d.tsファイルのinterfaceを利用して記述されていることがわかります。ジェネリクスでTとEが設定されておりEにはNativeMouseEventが設定されています。
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: string): boolean;
metaKey: boolean;
movementX: number;
movementY: number;
pageX: number;
pageY: number;
relatedTarget: EventTarget | null;
screenX: number;
screenY: number;
shiftKey: boolean;
}
先ほどのコードではMouseEventをreactからimportしているのでeventの型のMouseEventもジェネリクスのMouseEventもどちらもReactのMouseEventが設定されていることになります。エラーなしで動作した最初のコードはeventの型にはReactが持つMouseEventが設定され、ジェネリクスのMouseEventにはNativeのMouseEventが設定されていたため正しく動作します。ReactのMouseEventのジェネリクスにはNativeのEventの設定が必要であることが型定義を確認することでわかります。
ReactのMouseEventはindex.d.tsファイルに記述されていましたが、NativeのMouseEventはlib.dom.d.tsファイルの中に定義が記述されています。
interface MouseEvent extends UIEvent {
readonly altKey: boolean;
readonly button: number;
readonly buttons: number;
readonly clientX: number;
readonly clientY: number;
readonly ctrlKey: boolean;
readonly metaKey: boolean;
readonly movementX: number;
readonly movementY: number;
readonly offsetX: number;
readonly offsetY: number;
readonly pageX: number;
readonly pageY: number;
readonly relatedTarget: EventTarget | null;
readonly screenX: number;
readonly screenY: number;
readonly shiftKey: boolean;
readonly x: number;
readonly y: number;
getModifierState(keyArg: string): boolean;
/** @deprecated */
initMouseEvent(typeArg: string, canBubbleArg: boolean, cancelableArg: boolean, viewArg: Window, detailArg: number, screenXArg: number, screenYArg: number, clientXArg: number, clientYArg: number, ctrlKeyArg: boolean, altKeyArg: boolean, shiftKeyArg: boolean, metaKeyArg: boolean, buttonArg: number, relatedTargetArg: EventTarget | null): void;
}
MouseEventの型定義
MouseEventの型定義に戻りプロパティの情報を注目してみましょう。これらのプロパティを先ほどコンソールに表示されたeventオブジェクトの中身と比較してみてください。
下記のプロパティはすべてコンソールに表示されたイベントに含まれていることがわかります。
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: string): boolean;
metaKey: boolean;
movementX: number;
movementY: number;
pageX: number;
pageY: number;
relatedTarget: EventTarget | null;
screenX: number;
screenY: number;
shiftKey: boolean;
}
しかしコンソールに表示されているeventオブジェクトのプロパティの中でMouseEventの型定義に含まれていないものがあります。MouseEventのinterfaceがextendsでUIEventを継承しているのでUIEventの中身を確認します。
interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> {
detail: number;
view: AbstractView;
}
UIEventの型定義にはdetailとviewが存在し、コンソールに表示されるeventオブジェクトの中にdetailとviewが存在することが確認できます。
さらにUIEventはSyntheticEventを継承しているのでSynthenticEventを確認します。
/**
* currentTarget - a reference to the element on which the event listener is registered.
*
* target - a reference to the element from which the event was originally dispatched.
* This might be a child element to the element on which the event listener is registered.
* If you thought this should be `EventTarget & T`, see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/11508#issuecomment-256045682
*/
interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {}
SyntheticEventはBaseSyntheticEventを継承しているのでBaseSyntheticEventを確認します。BaseSyntheticEventを確認するとMouseEventとUIEventに含まれていなかったプロパティが含まれていることがわかります。
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;
}
型定義を確認していくとMouseEventの型とコンソールに表示されているeventオブジェクトを構成するプロパティが一致することがわかります。このことからeventの型にMouseEventの型を設定できることが理解できました。
HTMLButtonElementの理解
次はMouseEventのジェネリクスに設定していたHTMLButtonElementの理解を深めるためにdiv要素を使って動作確認を行います。
div要素を新たに追加してbutton要素と同様にonClickを追加し、同じ関数handleClickを実行できるように設定を行います。
import React from 'react';
import './App.css';
function App() {
const handleClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
console.log(event);
};
return (
<div className="App">
<button onClick={handleClick}>Click</button>
<div onClick={handleClick}>Click</div> //追加
</div>
);
}
export default App;
更新を保存すると下記のようにエラーが発生します。原因はジェネリクスに設定したHTMLButtonElementの定義に関連する問題であることがわかります。
div要素の場合はHTMLButtonElementではなくHTMLDivElementを設定する必要があります。eventのオブジェクトの中身のどこに違いがあるのか確認するためにdiv要素用にhandleDivClick関数を追加します。
import React from 'react';
import './App.css';
function App() {
const handleClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
console.log(event);
};
const handleDivClick = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
console.log(event);
};
return (
<div className="App">
<button onClick={handleClick}>Click</button>
<div onClick={handleDivClick}>Click</div>
</div>
);
}
export default App;
div要素の場合にはHTMLDivElementを設定することでエラーは解消されます。
button要素、div要素を順番にクリックしてコンソールに表示されているeventの中身を確認します。targetの部分を確認するとbutton, divに違いがあることがわかります
上記の結果だけ見るとHTMLButtonElementとHTMLDivElementの設定はtargetに関連すると思ってしまいますがbutton要素の中にspan要素を追加してボタンをクリックしてみます。
import React from 'react';
import './App.css';
function App() {
const handleClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
console.log(event);
};
const handleDivClick = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
console.log(event);
};
return (
<div className="App">
<button onClick={handleClick}>
<span>Click</span>
</button>
<div onClick={handleDivClick}>Click</div>
</div>
);
}
export default App;
targetにはbuttonではなくspanが表示されます。
targetとHTMLButtonElementが関連していないことを確認するためにHTMLButtonElementの型定義を確認します。定義の中にはdisabled, formActionなどが確認できます。
interface HTMLButtonElement extends HTMLElement {
disabled: boolean;
/** Retrieves a reference to the form that the object is embedded in. */
readonly form: HTMLFormElement | null;
/** Overrides the action attribute (where the data on a form is sent) on the parent form element. */
formAction: string;
/** Used to override the encoding (formEnctype attribute) specified on the form element. */
formEnctype: string;
/** Overrides the submit method attribute previously specified on a form element. */
formMethod: string;
/** Overrides any validation or required attributes on a form or form elements to allow it to be submitted without validation. This can be used to create a "save draft"-type submit option. */
formNoValidate: boolean;
/** Overrides the target attribute on a form element. */
formTarget: string;
//略
コンソールに表示されいているtargetはオブジェクトなので展開することができますが展開したプロパティの中にHTMLButtonElementの型定義に存在したdisabled、formActionなどを見つけることができません。この結果からtargetとHTMLButtonElementは関連していないことがわかります。
button要素の中に追加したspanを削除してボタンをクリックするとtargetがbuttonの場合にはdisabledやform, formActionを確認することができます。
ではHTMLButtonElementはどこの設定に影響を与えているのでしょう。
MouseEventの型定義を確認していきます。継承している型を確認していくとSyntheticEventが継承するBaseSyntheticEventのジャネリクスのCでMouseEventで渡したTの値が設定されていることがわかります。さらにCはtargetではなくcurrentTargetに設定されていることがわかります。targetにはTのEventTargetが設定されています。
//SynthenticEvent
interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {}
//BaseSyntheticEvent
interface BaseSyntheticEvent<E = object, C = any, T = any> {
nativeEvent: E;
currentTarget: C; //ここ
target: T;
bubbles: boolean;
//略
}
targetではなくcurrentTargetの型にHTMLButtonElementが利用されていることがわかりましたがコンソールに表示されているcurrentTargetはnullになり値を確認することができません。currentTargetの値を確認するためにhandleClick関数から直接event.currentTargetとevent.targetにアクセスします。
const handleClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
console.log(event.currentTarget);
console.log(event.target);
};
button要素の内側に<span>Click</span>を入れた状態でクリックを行います。currentTargetはonClickイベントを設定した要素そのものが取得されますがtargetではクリックした要素が表示されることがわかります。この結果からtargetとcurrentTargetの違いがわかりました。
HTMLButtonElementに含まれていたdisabledプロパティにcurrentTargetからはアクセスすることができますがtargetからアクセスしようとするとエラーが表示されます。
button要素からspanを要素を取り除いてもエラー内容が変わることはありません(EventTargetにはdisabledは存在しない)。span要素を取り除いてbutton要素が必ずevent.targetに含まれることがわかっている場合には下記のように型アサーション(as)を利用してtargetのdisabledを取得することができます。
const handleClick = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
console.log((event.target as HTMLButtonElement).disabled);
console.log(event.currentTarget.disabled);
};
MouseEventではジェネリクスを省略することもできますがdisabledにアクセスしようとした場合には下記のエラーメッセージが表示されます。HTMLButtonElementで定義されたプロパティにアクセスしたい場合にはHTMLButtonElementを設定する必要があります。
Property 'disabled' does not exist on type 'EventTarget & Element'.ts(2339)
ここまで理解できていれば、button, divのどちらの要素でもhandleClickを実行させても問題ない処理の場合はジェネリクスの設定を削除してもいいことがわかります。
import React from 'react';
import './App.css';
function App() {
const handleClick = (event: React.MouseEvent) => {
console.log(event);
};
return (
<div className="App">
<button onClick={handleClick}>Click</button>
<div onClick={handleClick}>Click</div>
</div>
);
}
export default App;
Union型を利用することもできます。
const handleClick = (
event: React.MouseEvent<HTMLButtonElement | HTMLDivElement, MouseEvent>
) => {
console.log(event);
};
さらにHTMLButtonElement, HTMLDivElementの継承元のHTMLElementも設定するも可能です。
const handleClick = (
event: React.MouseEvent<HTMLElement, MouseEvent>
) => {
console.log(event);
};
nativeEventの確認
MouseEventのジェネリクスのHTMLButtonElementの理解が進んだので再度NativeEventの設定を確認します。eventオブジェクトの中身を確認するとnativeEventにはMouseEventではなくPointerEventが設定されていることがわかります。
MouseEventからPointerEventに変更します。
const handleClick = (
event: React.MouseEvent<HTMLButtonElement, PointerEvent>
) => {
console.log(event);
};
特に設定変更することでこれまでの処理に変化はありません。PointerEventの型定義を確認するとMouseEventを継承し独自のプロパティを持っていることがわかります。
interface PointerEvent extends MouseEvent {
readonly height: number;
readonly isPrimary: boolean;
readonly pointerId: number;
readonly pointerType: string;
readonly pressure: number;
readonly tangentialPressure: number;
readonly tiltX: number;
readonly tiltY: number;
readonly twist: number;
readonly width: number;
/** Available only in secure contexts. */
getCoalescedEvents(): PointerEvent[];
getPredictedEvents(): PointerEvent[];
}
試しにhandleClick関数の中からpointerIdにアクセスしてみましょう。
const handleClick = (
event: React.MouseEvent<HTMLButtonElement, PointerEvent>
) => {
console.log(event.nativeEvent.pointerId);
};
ブラウザに表示されるボタンをクリックするとコンソールには1の数字が表示されます。PointerEventからMouseEventに変更します。これまでの流れを理解していれば想像がつくと思いますがエラーが発生します。
Compiled with problems:X
ERROR in src/App.tsx:8:35
TS2339: Property 'pointerId' does not exist on type 'MouseEvent'.
6 | event: React.MouseEvent<HTMLButtonElement, MouseEvent>
7 | ) => {
> 8 | console.log(event.nativeEvent.pointerId);
| ^^^^^^^^^
9 | };
10 |
11 | return (
明確にMouseEventの型にはpointerIdのプロパティは存在しないと言ってくれているのでわかりやすいです。pointerIdの用途はわかりませんがもしpointerIdが必要になる場合にはMouseEventではなくPointerEventを設定する必要があるということがわかります。
input要素からのイベント
利用するコード
利用するコードはinput要素にonChangeイベントを設定して文字をinput要素に入力するとhandleChange関数が実行されるシンプルなコードから始めていきます。
import { useState } from 'react';
function App() {
const [name, setName] = useState('');
const handleChange = (event) => {
setName(event.target.value);
};
return (
<div className="App">
<input value={name} onChange={handleChange} />
<div>name:{name}</div>
</div>
);
}
export default App;
JavaScript環境であれば上記のコードは問題なく実行できますがTypeScript環境ではnpm start/yarn startコマンドを実行するとブラウザ上に以下のエラーが表示されます。eventに型が設定されていないことが原因です。
Compiled with problems:X
ERROR in src/App.tsx:26:25
TS7006: Parameter 'event' implicitly has an 'any' type.
24 | const [name, setName] = useState('');
25 |
> 26 | const handleChange = (event) => {
| ^^^^^
27 | setName(event.target.value);
28 | };
29 |
eventの型を確認するために先ほどと同様にTypeScriptの推論とVSCodeの力を借りて型を確認します。型はReact.ChangeEvent<HTMInputElement>であることがわかります。
表示されている型(React.ChangeEvent<HTMInputElement>)の設定を行うとコードは正常に動作します。
event.target.valueへのアクセス
MouseEventの場合はevent.taget.valueにアクセスする場合には型アサーションを利用しましたが今回は型エラーもなくアクセスできています。
import { useState } from 'react';
export default function App() {
const [name, setName] = useState('');
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
};
return (
<div className="App">
<input value={name} onChange={handleChange} />
<div>name:{name}</div>
</div>
);
}
その理由を探るためChangeEventの型定義を確認します。MouseEventよりもシンプルでジェネリクスにはTのElementしか取りません。またSyntheticEventをextends(継承)していますがSyntheticEventの定義の中でtargetをEventTargetとTのIntersect typesで設定しています。このためHTMLInputElementの型がtargetに設定されているのでevent.target.valueでアクセスしてもエラーが表示されることはありません。HTMLInputElementの型定義にvalueが含まれているためです。
interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
target: EventTarget & T;
}
ChangeEventのジェネリクスからHTMLInputElementを削除した場合にはどのようなエラーが発生するか確認します。target.valueへのアクセスでエラーとなります。EventTarget & Elementではtarget.valueの型が定義されていないためです。
Compiled with problems:X
ERROR in src/App.tsx:26:26
TS2339: Property 'value' does not exist on type 'EventTarget & Element'.
24 |
25 | const handleChange = (event: React.ChangeEvent) => {
> 26 | setName(event.target.value);
| ^^^^^
27 | };
28 |
29 | return (
このことからinput要素の場合はvalueへのアクセスが必須となるのでChangeEventを設定する場合はHTMLInputElementの設定が必須になることがわかります。
inlineの記述による型推論
またonChangeイベントの処理をインラインで記述すると型推論により型を指定しなくてもエラーが表示されることはありません。
import { useState } from 'react';
function App() {
const [name, setName] = useState('');
return (
<div className="App">
<input value={name} onChange={(event) => setName(event.target.value)} />
<div>name:{name}</div>
</div>
);
}
export default App;
input要素からtextarea要素への変更も簡単に行うことができます。
import { useState } from 'react';
function App() {
const [name, setName] = useState('');
return (
<div className="App">
<textarea value={name} onChange={(event) => setName(event.target.value)} />
<div>name:{name}</div>
</div>
);
}
export default App;
インラインではなくhandleChange関数を利用する場合はtextareaの場合はchangeEventのジェネリクスでHTMLTextAreaElementを設定することで型の設定を行うことができます。
ここまでの流れを通してReact環境でTypeScriptを利用する場合のeventの型の設定について少しだけ理解が深まったのではないでしょうか。