typescripttypescreateelementconditional-types

Return a conditional TypeScript declaration from another function


TypeScript knows that globalThis.document.createElement('img') returns a type of HTMLImageElement based on the string 'img' being passed into the function.

const elem1 = globalThis.document.createElement('img');  //type: HTMLImageElement
console.log(elem1.constructor.name);  //'HTMLImageElement'

How would I capture that return type to be used in a wrapper function?

For example, what would the TypeScript declaration be for the createElem function below so that the tag parameter determines the correct return type?

const createElem = (tag: keyof HTMLElementTagNameMap) => {
   const elem = globalThis.document.createElement(tag);
   elem.dataset.created = String(Date.now());
   return elem;
   };

const elem2 = createElem('img');  //type: HTMLElement | ...68 more...
console.log(elem2.constructor.name);  //'HTMLImageElement'

A tag value of ’img’ should result in a HTMLImageElement type. A tag value of ’p’ should result in a HTMLParagraphElement type and so on.


Solution

  • If you check the official type definition of createElement you will see:

    createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K];
    

    Let's apply the same logic to your function:

    const createElem = <K extends keyof HTMLElementTagNameMap>(
      tag: K,
    ): HTMLElementTagNameMap[K] => {
      const elem = globalThis.document.createElement(tag);
      elem.dataset.created = String(Date.now());
      return elem;
    };
    

    Testing:

    const elem2 = createElem('img');  // HTMLImageElement
    const elem3 = createElem('p');  // HTMLParagraphElement
    console.log(elem2.constructor.name);
    

    Link to Playground

    Additionally, createElement supports passing any other strings as well, which is achieved using function overloading:

        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;
    

    You can do the same thing for your function if you need that. Note that the order of overloads is important. The most defined one should come first. Which is keyof HTMLElementTagNameMap in your case. The reason is typescript looks through the overloads from top to bottom and if you put the overload with string first then it would reach the keyof HTMLElementTagNameMap.