< Home

Generics in Typescript & React

Learn how we can use generics in functions to improve type-safety, and further expand that into React components and hooks.

January 06, 2022 (Updated on January 10, 2022)

Overview

Generics can be a difficult concept for some, but when used correctly, can improve re-usability and type safety within your applications.

Basics

You can think of generics as a placeholder type, that can be specific to your implenentation when you use it. Unlike any and unknown, generics provide flexibility without sacrificing on type safety.

Here, we'll use an example function called split. This function takes an array, and splits it's content into two separate arrays and returns them.

export const split = (items: any[]): [any[], any[]] => {
    const setOne = [];
    const setTwo = [];

    items.map((i, index) => {
        if (index % 2 == 0) {
            setOne.push(i);
        } else {
            setTwo.push(i);
        }
    });

    return [setOne, setTwo];
};

While we could use any here, we would lose out on type safety when interacting with the returning data. Instead, we should make use of generics to indicate the types are determined on what is being passed into the function. For example, we would adjust our code as such:

export const split = <T extends any>(items: T[]): [T[], T[]] => {
    const setOne: T[] = [];
    const setTwo: T[] = [];

    items.map((i, index) => {
        if (index % 2 == 0) {
            setOne.push(i);
        } else {
            setTwo.push(i);
        }
    });

    return [setOne, setTwo];
};

With this code, passing a string[] will return a [string[], string[]]; the types are inferred from the data being passed in. Generic parameters don't have to be called T, although convention is to start the parameter name with T, such as TData.

Constrainted types

Generic types can be constrained to only allow a subset of a type to use the function. This can be useful if you want to perform actions on a property of an object, but don't know what the implemented object will look like.

type MyType = {
    id: number;
};

type InheritedType = MyType & {
    name: string;
    birthday: Date;
};

export const split = <T extends MyType>(items: T[]): [T[], T[]] => {
    const setOne: T[] = [];
    const setTwo: T[] = [];

    items.map((i, index) => {
        // We can safely use i.id here, since it's guaranteed to exist from the constraint.

        if (index % 2 == 0) {
            setOne.push(i);
        } else {
            setTwo.push(i);
        }
    });

    return [setOne, setTwo];
};

Objects of MyType, InheritedType and any type with an id: number property would be valid to pass into this function, thanks to the constraint.

Inferred variables

Some generic functions require the developer to be explicit in what types to use. Others are able to infer automatically, based on the data being passed in.

For instance, the following hook doesn't have a variable to infer types from, so the type is required when the hook is used:

export const useDistanceFromTop = <TElement extends HTMLElement>(): [
    MutableRefObject<TElement>,
    { distance: number }
] => {
    const ref = useRef<TElement>();
    const distance = useIfClient(
        () => (ref.current?.getBoundingClientRect().top ?? 0) + (document?.documentElement.scrollTop ?? 0),
        0
    );

    return [ref, { distance }];
};

Automatic and manual type inference in the same function doesn't currently exist, so even if you had a variable that could be inferred, if you're having to define one type, you need to define them all.

Generics in Components

Generics can also be used within React components, although their functionality is limited as generic types don't pass down through the Context API. One such use-case might be using a function as a child, as shown below:

type Props<T> = {
    items: T[]
    children: (item: T) => JSX.Element
}

export function ExampleComponent<T>({ items, children }: Props<T>) {
    return <div>
        {items.map(i => children(i))}
    </div>
}

//To use like...
items = [
    { id: 1 },
    { id: 2 },
]

<ExampleComponent items={items}>{(item) => <div>{item.id}</div></ExampleComponent>

This would allow you to create a component with re-usable functionality (such as a checkbox list), but customise the content you're rendering.

Generics in Hooks

Hooks can also make use of generics to provide re-usable functionality alongside React lifecycle events. Some examples can be found in my code snippets blog post, with the below examples pulled from it:

type UseRelativeScrollPercentageResult<TStartElement, TEndElement> = [
    MutableRefObject<TStartElement>,
    MutableRefObject<TEndElement>,
    { percentage: number }
];

export const useRelativeScrollPercentage = <
    TStartElement extends HTMLElement = HTMLElement,
    TEndElement extends HTMLElement = HTMLElement
>(
    offset = 0
): UseRelativeScrollPercentageResult<TStartElement, TEndElement> => {
    const [fromRef, { distance: fromDistance }] = useDistanceFromTop<TStartElement>();
    const [toRef, { distance: toDistance }] = useDistanceFromTop<TEndElement>();
    const { y } = useWindowScroll();

    const current = y - fromDistance + offset;
    const end = toDistance - fromDistance;

    const percentage = useMemo(() => {
        return (current / end) * 100;
    }, [current, end, y]);

    return [fromRef, toRef, { percentage }];
};

The above example uses constrained generic types to restrict the types to HTMLElements, allowing it to be used with any HTML tag while maintaining the correct ref type.

export const useFilter = <TFilter, TData>(
    data: TData[],
    filter: TFilter = undefined,
    predicate: (data: TData, filter: TFilter) => boolean
): TData[] => {
    return useMemo(() => {
        if (!filter || (Array.isArray(filter) && filter.length === 0)) {
            return data;
        }

        return data.filter((entry) => predicate(entry, filter));
    }, [filter, data, predicate]);
};

This example infers the TData and TFilter types automatically from the parameters passed into it, and applies those inferred types to the return type.

Conclusion

I hope after reading this article, generics are clearer to use in how they are built, and the functionality they can provide. As always, send any questions you have to the Q&A page and I'll get in touch.

Related Posts

Hello World!

Hello World!

Hello World! A brief introduction to the blog, what I'm planning, and more.

How to get more from your GraphQL data in Gatsby

How to get more from your GraphQL data in Gatsby

A guide for Gatsby on how to get more out of your GraphQL data.

Building a Design System; 👍's & 👎's

Building a Design System; 👍's & 👎's

What you should and shouldn't do when building a design system, from theming, to styling, to component structure and testing.

Code Snippets - React Hooks

Code Snippets - React Hooks

A selection of React utility hooks I've put together over the years.