Home > Back-end >  How can I create a reusable get element function using typescript generics?
How can I create a reusable get element function using typescript generics?

Time:02-11

I'm trying to merge the selectElement and getElement functions into a reusable typescript generic function where I can use select the element(s) and still be able to call them as the following...

const btnArray = getElementTS<HTMLButtonElement>('.btn', '.container', true)

or const singleBtn = getElementTS<HTMLButtonElement>('.btn', '.container')

P.S. Forgive any errors I might have made. I'm just a newbie.

   // this function returns a single element
   const selectElement = (selector, scope) => {
      return (scope || document).querySelector(selector);
   };
    
   // this function returns either a single element or an element array
   function getElement(selector, isList) {
       let element = isList
       ? [...document.querySelectorAll(selector)]
       : document.querySelector(selector);

  if ((!isList && element) || (isList && !element.length < 1)) return element;
  throw new Error(`Please double check your selector : ${selector}`);
}

interface Length {
  length: number;
}

// merging the two functions with a typescript version
function getElementTS<E extends Length & HTMLElement & string>(
  selector: string,
  scope: E,
  isList: boolean
) {
  let element = isList
    ? ([...(scope || document).querySelectorAll(selector)] as E[])
    : ((scope || document).querySelector(selector) as E);

  if ((!isList && el) || (isList && !element.length < 1)) return el;
  throw new Error(`Please double check your selector : ${selector}`);
}

console.log(getElementTS('.btn', '.main' ,true));

CodePudding user response:

In order to be explicit about what is returned based on the value of isList, I would suggest rewriting the logic in your function as such:

const parsedSelector = scope ? `${scope} ${selector}` : selector;
try {
    if (isList) {
        const element = [...document.querySelectorAll(parsedSelector)] as T[];
        if (element.length < 1) throw Error;
        return element;
    } else {
        const element = document.querySelector(parsedSelector) as T;
        if (!element) throw Error;
        return element;
    }
} catch(e) {
     throw new Error(`Please double check your selector : ${selector}`);
}

By doing this, you won't run into issues where TypeScript fails to narrow the type for element in this potentially problematic line:

if ((!isList && el) || (isList && !element.length < 1)) return el;

Moreover, it is necessary to create a parsedSelector to ensure that the scope and selector variables are properly concatenated. Using scope.querySelectorAll() in your original code will throw an error because scope is a string and not an Element.

Then, it is just a matter of adding function overloads to ensure proper correspondence between the provided arguments and the expected return type:

function getElementTS<T extends Element>(selector: string, scope: string): T
function getElementTS<T extends Element>(selector: string, scope: string, isList: true): T[]
function getElementTS<T extends Element>(selector: string, scope: string, isList: false): T
function getElementTS<T extends Element>(
    selector: string,
    scope: string,
    isList?: boolean
): T | T[] {
    // Function logic here
}

See example on TypeScript playground.

CodePudding user response:

This is a revised solution from the answer to my earlier question. The purpose of this function is to get an element or elements from the DOM and throw a useful exception if they are not found

function getElement<T extends Element>(
  selector: string,
  scope: ParentNode | Document
): T;
function getElement<T extends Element>(
  selector: string,
  scope: ParentNode | Document,
  isElementArray: true
): T[];
function getElement<T extends Element>(
  selector: string,
  scope: ParentNode | Document,
  isElementArray: false
): T;
function getElement<T extends Element>(
  selector: string,
  scope: ParentNode | Document,
  isElementArray?: boolean
): T | T[] {
  try {
    if (isElementArray) {
      const element = [...scope.querySelectorAll(selector)] as T[];
      if (element.length < 1) throw Error;
      return element;
    } else {
      const element = scope.querySelector(selector) as T;
      if (!element) throw Error;
      return element;
    }
  } catch (e) {
    throw new Error(
      `There is an error. Please check if your selector: ${selector} is correct`
    );
  }
}

Usage:

/* returns HTMLButtonElement */
const tabList = getElement<HTMLDivElement>('.tab-list', document, false)
const tabButton = getElement<HTMLButtonElement>('.tabBtn', tabList')
const navBtn = getElement('.btn--mobile-toggle', document) as HTMLButtonElement

/* returns HTMLButtonElement[] i.e Array<HTMLButtonElement>*/
const tabButtons = getElement<HTMLButtonElement>('.tabBtn', tabList, true)
const logos = getElement('.logo', document, true) as HTMLImageElement[]

See example on Typescript Playground

  • Related