How to Create a Generic Function
A recipe for writing reusable, generic functions in TypeScript that can work with multiple types while keeping full type safety.
Sometimes you write a function that does the same thing for different types. Instead of writing the function multiple times, you can use generics to make it work for any type you give it, while keeping it type-safe.
A common use case is when you have to repeat the same checks. For example, when selecting DOM elements, you always have to check if they exist before you can use them.
This pattern of selecting an element and then checking if it’s null can get repetitive.
const container = document.querySelector<HTMLDialogElement>(
"#search-box-container"
);
if (container == null) {
throw Error("search-box-container not found");
}
const button = document.querySelector<HTMLButtonElement>("#search-button");
if (button == null) {
throw Error("search-button not found");
}
We can fix this by creating a single, generic helper function.
This function, getElement
, will find an element and throw an error if it’s not
found. It’s “generic” because it can return any kind of element, and TypeScript
will know the exact type.
function getElement<T extends Element>(selector: string): T {
const element = document.querySelector<T>(selector);
if (element == null) {
throw Error(`Element not found for selector: ${selector}`);
}
return element;
}
How it works:
-
<T extends Element>
: This is the key part that makes the function generic.<T>
declares a generic type parameter namedT
. Think of it as a variable for types that the caller will provide.extends Element
is a “generic constraint”. It tells TypeScript thatT
can be any type, as long as it’s a type ofElement
(likeHTMLButtonElement
).
-
document.querySelector<T>(selector)
: We use our generic typeT
to tellquerySelector
what kind of element to expect. It returns the typeT | null
. -
if (element == null)
: This is a “type guard”. After this check, TypeScript is smart enough to know thatelement
can’t benull
anymore. It narrows the type fromT | null
to justT
.
Now your main code is much cleaner and easier to read, with no more null checks.
function main() {
const container = getElement<HTMLDialogElement>("#search-box-container");
const button = getElement<HTMLButtonElement>("#search-button");
// You can now use 'container' and 'button' safely.
button.addEventListener("click", () => {
container.showModal();
});
}
document.addEventListener("DOMContentLoaded", main);