10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TypeScriptでカスタムイベントの型定義をする方法

Posted at

はいさい!ちゅらデータぬオースティンやいびーん!

概要

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>;
  }
}

こうすると、以下のようにWindowaddEventListenerを実行すると、CustomEvent"user-login"が自動で推測される上、EventListenerevent.detailも文字列型だと推測されます。

スクリーンショット 2022-10-06 14.42.24.png

定義しているファイル以外にもちゃんと拾われます。

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にあるとしましょう。

login-form.ts
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構文を追加すると、違うスクリプトでも型推測ができます。

login-form.ts
declare global {
    interface HTMLElementTagNameMap {
        [tagName]: LoginForm;
    }
    interface HTMLElementEventMap {
        "user-login": CustomEvent<string>;
    }
}

HTMLElementTagNameMapも拡張していますが、これは<login-form>を一つのHTMLElementとしてTypeScriptのインタープリターに知らせる、つまり型定義するためです。

違うスクリプトのindex.tsで以下のようなコードが問題なく書けます。

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を使っているのですよ?

10
4
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
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?