3
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

型定義ファイルで学ぶTypeScript 〜Document.createElementとString Literal Typesとkeyof〜

Document.createElementのMDNドキュメントはこちら

Document.createElementは、引数としてタグ名を文字列リテラルで渡すと、それに該当するタグのオブジェクトが帰ってきます。

文字列リテラル"a"を引数として渡すと、返り値のオブジェクトはHTMLAnchorElementに推論されます。"p"ならばHTMLParagraphElementに、"span"ならばHTMLSpanElementに、そのタグ名に相当する型に返り値型が推論されます。

const a  = document.createElement("a"); // HTMLAnchorElementと推論される
const p = document.createElement("p"); // HTMLParagraphElementと推論される
const span  = document.createElement("span"); // HTMLSpanElementと推論される

TypeScriptの公式サイトの「Advanced Type」の「String Literal Types」には、このメソッドがString Literal Typesとオーバーロードを用いた活用例として載っています。

TypeScript v2.1.2のib/lib.dom.d.tsより一部。

    createElement(tagName: "a"): HTMLAnchorElement;
    createElement(tagName: "applet"): HTMLAppletElement;
    createElement(tagName: "area"): HTMLAreaElement;
    ~~
    createElement(tagName: "p"): HTMLParagraphElement;
    createElement(tagName: "param"): HTMLParamElement;
    createElement(tagName: "picture"): HTMLPictureElement;
    createElement(tagName: "pre"): HTMLPreElement;
    createElement(tagName: "progress"): HTMLProgressElement;
    ~~
    createElement(tagName: "x-ms-webview"): MSHTMLWebViewElement;
    createElement(tagName: "xmp"): HTMLPreElement;
    createElement(tagName: string): HTMLElement;

このように、TypeScript v2.1.2までは「String Literal Types」とオーバーロードを駆使して、型定義ファイルが作成されていました。


ところが、TypeScript v2.1.4でDocument.createElementの型定義が変わります。「String Literal Types」を使っていません。こちらのコミットで変わったようです。

次のような型定義となっています。

TypeScript v2.1.4のib/lib.dom.d.tsより一部。

    createElement<K extends keyof HTMLElementTagNameMap>(tagName: K): HTMLElementTagNameMap[K];
    createElement(tagName: string): HTMLElement;

HTMLElementTagNameMapは次のようなインターフェースです。

interface HTMLElementTagNameMap {
    "a": HTMLAnchorElement;
    "applet": HTMLAppletElement;
    "area": HTMLAreaElement;
    ~~
    "p": HTMLParagraphElement;
    "param": HTMLParamElement;
    "picture": HTMLPictureElement;
    "pre": HTMLPreElement;
    "progress": HTMLProgressElement;
    ~~
    "x-ms-webview": MSHTMLWebViewElement;
    "xmp": HTMLPreElement;
}

TypeScript 2.1から導入された keyofとHTMLElementTagNameMapを使った型定義ファイルに変わりました。

このようにkeyofを活用することで

  • "a" ならば HTMLAnchorElement
  • "p" ならば HTMLParagraphElement
  • "span" ならば HTMLSpanElement

という型推論が聞くようになります。


記事執筆時の最新の型定義ファイル「TypeScript v3.5.3のlib/lib.dom.d.ts」では、さらに改良が進んでいます。

HTMLElementTagNameMapは、createElementだけでなく、getElementsByTagNamequerySelectorでも活用されています。

/**
* Creates an instance of the element for the specified tag.
* @param tagName The name of an element.
*/
createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K];
/** @deprecated */
createElement<K extends keyof HTMLElementDeprecatedTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementDeprecatedTagNameMap[K];
createElement(tagName: string, options?: ElementCreationOptions): HTMLElement;
/**
* Retrieves a collection of objects based on the specified element name.
* @param name Specifies the name of an element.
*/
getElementsByTagName<K extends keyof HTMLElementTagNameMap>(qualifiedName: K): HTMLCollectionOf<HTMLElementTagNameMap[K]>;
getElementsByTagName<K extends keyof SVGElementTagNameMap>(qualifiedName: K): HTMLCollectionOf<SVGElementTagNameMap[K]>;
getElementsByTagName(qualifiedName: string): HTMLCollectionOf<Element>;
/**
* Returns the first element that is a descendant of node that
* matches selectors.
*/
querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;
querySelector<K extends keyof SVGElementTagNameMap>(selectors: K): SVGElementTagNameMap[K] | null;
querySelector<E extends Element = Element>(selectors: string): E | null;
/**
* Returns all element descendants of node that
* match selectors.
*/
querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;
querySelectorAll<K extends keyof SVGElementTagNameMap>(selectors: K): NodeListOf<SVGElementTagNameMap[K]>;
querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;

このようにタグ名の文字列リテラルと型の結びつけをHTMLElementTagNameMapに集約することで、String Literal Typesよりもエレガントに型を定義することができます。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
3
Help us understand the problem. What are the problem?