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

`Document.createElement`のMDNドキュメントは[こちら](https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement)


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

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

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

---

TypeScriptの公式サイトの「Advanced Type」の「[String Literal Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types)」には、このメソッドがString Literal Typesとオーバーロードを用いた活用例として載っています。

[TypeScript v2.1.2のib/lib.dom.d.ts](https://github.com/microsoft/TypeScript/blob/v2.1.2/lib/lib.dom.d.ts#L2809-L2889)より一部。

```typescript
    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」を使っていません。[こちら](https://github.com/microsoft/TypeScript/commit/b624d4781705badb8eab3375865ebb83a6ae5b8a#diff-46fd87925e4552c166ec188712741c3f)のコミットで変わったようです。

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

[TypeScript v2.1.4のib/lib.dom.d.ts](https://github.com/microsoft/TypeScript/blob/v2.1.4/lib/lib.dom.d.ts#L2916-L2917)より一部。


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

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

```typescript
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から導入された](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types) keyofと`HTMLElementTagNameMap`を使った型定義ファイルに変わりました。

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

* `"a"` ならば `HTMLAnchorElement`
* `"p"` ならば `HTMLParagraphElement`
* `"span"` ならば `HTMLSpanElement`

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

---

記事執筆時の最新の型定義ファイル「[TypeScript v3.5.3のlib/lib.dom.d.ts](https://github.com/microsoft/TypeScript/blob/v3.5.3/lib/lib.dom.d.ts#L4440-L4443)」では、さらに改良が進んでいます。


`HTMLElementTagNameMap`は、`createElement`だけでなく、`getElementsByTagName`や`querySelector`でも活用されています。

```typescript
/**
* 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;
```

```typescript
/**
* 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>;
```

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