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[]