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.

TypeScript
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.

TypeScript
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:

  1. <T extends Element>: This is the key part that makes the function generic.

    • <T> declares a generic type parameter named T. Think of it as a variable for types that the caller will provide.
    • extends Element is a “generic constraint”. It tells TypeScript that T can be any type, as long as it’s a type of Element (like HTMLButtonElement).
  2. document.querySelector<T>(selector): We use our generic type T to tell querySelector what kind of element to expect. It returns the type T | null.

  3. if (element == null): This is a “type guard”. After this check, TypeScript is smart enough to know that element can’t be null anymore. It narrows the type from T | null to just T.

Now your main code is much cleaner and easier to read, with no more null checks.

TypeScript
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);

Other recipes in Functions