Home > OS >  can i refactor the select and get element functions into one with typescript generics?
can i refactor the select and get element functions into one with typescript generics?

Time:02-02

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.

  •  Tags:  
  • Related