LoginSignup
11
3

More than 3 years have passed since last update.

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

Posted at

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よりもエレガントに型を定義することができます。

11
3
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
11
3