はいさい!ちゅらデータぬオースティンやいびーん!
概要
TypeScriptでカスタムイベント(CustomEvent)の型を定義する方法を紹介します。
CustomEvent定義が必要な要素は以下の三つかと思います。
Window
Document
HTMLElement
それぞれのEventEmitter
のTypeScriptのデフォルトのEvent定義を拡張する方法を解説していきましょう!
WindowのCustomEventを登録する
まず、Window
に対してCustomEventを配信するようなコードがあった場合の解説をします。
Window
が配信元の場合、WindowEventMap
というTypeScriptの中に入っている定義を拡張する必要があります。
拡張方法はdeclare global
構文になります。
export const emitUserLoginEvent = (uid: string) => {
const event = new CustomEvent("user-login", { detail: uid });
window.dispatchEvent(event);
}
declare global {
interface WindowEventMap {
"user-login": CustomEvent<string>;
}
}
こうすると、以下のようにWindow
にaddEventListener
を実行すると、CustomEvent
の"user-login"
が自動で推測される上、EventListener
のevent.detail
も文字列型だと推測されます。
定義しているファイル以外にもちゃんと拾われます。
window.addEventListener("user-login", ({ detail }) => {
console.log(detail) // string
})
通常であれば、ここでTypeScriptのインタプリーターが文句を言い始めるのですが、きちんとWindowEventMap
を拡張していれば納得してくれます。
TSプレーグラウンドでもご確認いただけます。
DocumentのCustomEventを定義する方法
上記と似たような手法になりますが、今回はDocumentEventMap
というinterface
を拡張します。
非常に近いコードになりますが、以下の通りです。
export const emitUserLoginEvent = (uid: string) => {
const event = new CustomEvent("user-login", { detail: uid });
document.dispatchEvent(event);
}
declare global {
interface DocumentEventMap {
"user-login": CustomEvent<string>;
}
}
こちらもTSプレーグラウンドでご確認いただけます。
HTMLElementのCustomEventを定義する方法
最後に、こちらは筆者にとっては結構重要なものですが、HTMLElement
から配信されるCustomEventも定義できます。
HTMLElement
というと、全てのHTML要素になってしまうのですが、HTMLElement
を継承して定義されるWeb Componentsもこの手法で定義できるのです。
Web ComponentでCustomEventを配信することも多いかと思いますが、以下のやり方で型定義ができます。
以下のようなWeb ComponentがDOMにあるとしましょう。
export const tagName = "login-form";
export class LoginForm extends HTMLElement {
form!: HTMLFormElement;
connectedCallback() {
this.innerHTML = `
<form>
<input name="username">
<input name="password">
<form>`
this.form = this.querySelector("form")!;
this.form.addEventListener("submit", this.handleFormSubmission);
}
private handleFormSubmission: EventListener = (event) => {
event.preventDefault();
new Promise((resolve, reject) => {
// 認証ロジック
resolve("uid");
}).then((uid) => {
const event = new CustomEvent("user-login", { detail: uid, bubbles: false });
this.dispatchEvent(event);
})
}
}
if (!window.customElements.get(tagName)) {
window.customElements.define(tagName, LoginForm);
}
ログインしたら、<login-form>
から"user-login"
のイベントが配信されるようになっています。
すると、以下のようなdeclare global
構文を追加すると、違うスクリプトでも型推測ができます。
declare global {
interface HTMLElementTagNameMap {
[tagName]: LoginForm;
}
interface HTMLElementEventMap {
"user-login": CustomEvent<string>;
}
}
HTMLElementTagNameMap
も拡張していますが、これは<login-form>
を一つのHTMLElement
としてTypeScriptのインタープリターに知らせる、つまり型定義するためです。
違うスクリプトのindex.ts
で以下のようなコードが問題なく書けます。
import "./login-form.js";
const loginForm = document.createElement(tagName);
loginForm.addEventListener("user-login", ({ detail }) => {
console.log(detail) // string
})
まとめ
以上、CustomEvent
のTypeScriptにおける型定義の方法を紹介してまいりました。
特にWeb Componentを書くときに、こういうdeclare global
構文を追加すると、アプリケーション全体の型推測が良くなり、開発がスムーズになるかと思います。
また、Web ComponentsとVueを一緒に使っている時もCustomEventsを使って組み込みますが、型定義をしていると円滑に導入できます。
脱線ですが、実際、VueはCustomEvent
を使っているのですよ?