Return a conditional TypeScript declaration from another function

82 Views Asked by At

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.

2

There are 2 best solutions below

0
wonderflame On BEST ANSWER

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.

0
SandStone On

Here's my approach just give your function the same type the function you want to wrap using typeof, then specify the parameters to be the same as the function you want to wrap using Parameters utility type.

const a = document.querySelector('img');
const createElem:typeof document.createElement = (
  ...args: Parameters<typeof document.createElement>
) => {
  const tag = args[0];
  const elem = globalThis.document.createElement(tag);
  elem.dataset.created = String(Date.now());
  return elem;
};

const elem2 = createElem('img');  // HTMLImageElement
const elem3 = createElem('p');  // HTMLParagraphElement

Link to the playground

This also has the advantange of not having to declare the type again, which would be useful for this case since createElement has many overloads