DEV Community

Cover image for Useful TypeScript generics for tree structures
Marat Sabitov
Marat Sabitov

Posted on

Useful TypeScript generics for tree structures

Developers are surrounded by hierarchies of objects, such as the DOM tree, React component tree, and NPM dependency tree. Tree-like data structures are quite common in code, and reliable Typescript types make working with them more confident. In this article, I want to share with you some generics that benefit me the most.

Deep partial

Sometimes we are willing to provide default values for all parameters of a third-party function, even nested ones, so they are all optional. In that case DeepPartial type is helpful:

type DeepPartial<T> = T extends object
    ? {
          [P in keyof T]?: DeepPartial<T[P]>;
      }
    : T;

type InitialType = {
    header: {
        size: 'sm' | 'md' | 'lg';
        color: 'primary' | 'secondary';
        nav: {
            align: 'left' | 'right';
            fontSize: number;
        }
    };
    footer: {
        fixed: boolean;
        links: {
            max: 5 | 10;
            nowrap: boolean;
        }
    }
};

type ResultType = DeepPartial<InitialType>;
/*
type ResultType = {
    header?: {
        size?: "sm" | "md" | "lg" | undefined;
        color?: "primary" | "secondary" | undefined;
        nav?: {
            align?: "left" | "right" | undefined;
            fontSize?: number | undefined;
        } | undefined;
    } | undefined;
    footer?: {
        fixed?: boolean | undefined;
        links?: {
            max?: 5 | 10 | undefined;
            nowrap?: boolean | undefined;
        } | undefined;
    } | undefined;
};
*/
Enter fullscreen mode Exit fullscreen mode

Paths

Another case is that you need to infer all the available tree paths as a type. Paths generic can solve this:

type Paths<T> = T extends object ? {
        [K in keyof T]: `${Exclude<K, symbol>}${
            '' | Paths<T[K]> extends '' ? '' : `.${Paths<T[K]>}`
        }`;
    }[keyof T] : never;

type InitialType = {
    header: {
        size: 'sm' | 'md' | 'lg';
        color: 'primary' | 'secondary';
        nav: {
            align: 'left' | 'right';
            fontSize: number;
        }
    };
    footer: {
        fixed: boolean;
        links: {
            max: 5 | 10;
            nowrap: boolean;
        }
    }
};

type ResultType = Paths<InitialType>;
/*
type ResultType = "header.size" | "header.color" | "header.nav.align" |
"header.nav.fontSize" | "footer.fixed" | "footer.links.max" |
"footer.links.nowrap";
*/
Enter fullscreen mode Exit fullscreen mode

Sometimes path should include leaves, so you can use the next generic:

type Paths<T> = T extends object ? {
        [K in keyof T]: `${Exclude<K, symbol>}${
            '' | Paths<T[K]> extends '' ?
                '' :
                `.${Paths<T[K]>}`
            }`;
    }[keyof T] : T extends string | number | boolean ? T : never;

type InitialType = {
    header: {
        size: 'sm' | 'md' | 'lg';
        color: 'primary' | 'secondary';
        nav: {
            align: 'left' | 'right';
            fontSize: number;
        }
    };
    footer: {
        fixed: boolean;
        links: {
            max: 5 | 10;
            nowrap: boolean;
        }
    }
};

type ResultType = Paths<InitialType>;
/*
type ResultType = "header.size.sm" | "header.size.md" | "header.size.lg" |
"header.color.primary" | "header.color.secondary" | "header.nav.align.left" |
"header.nav.align.right" | `header.nav.fontSize.${number}` |
"footer.fixed.false" | "footer.fixed.true" | "footer.links.max.5" |
"footer.links.max.10" | "footer.links.nowrap.false" |
"footer.links.nowrap.true";
*/
Enter fullscreen mode Exit fullscreen mode

You may notice a strange element header.nav.fontSize.${number} in the ResultType. It appears due to the fact that the fontSize parameter can take an infinite number of values. We can modify the generic to eliminate such paths:

type Paths<T> = T extends object ? {
        [K in keyof T]: `${Exclude<K, symbol>}${
            '' | Paths<T[K]> extends '' ? '' : `.${Paths<T[K]>}`
        }`;
    }[keyof T] : T extends string | number | boolean ?
    `${number}` extends `${T}` ? never : T :
    never;

type InitialType = {
    header: {
        size: 'sm' | 'md' | 'lg';
        color: 'primary' | 'secondary';
        nav: {
            align: 'left' | 'right';
            fontSize: number;
        }
    };
    footer: {
        caption: string;
        fixed: boolean;
        links: {
            max: 5 | 10;
            nowrap: boolean;
        }
    }
};

type ResultType = Paths<InitialType>;
/*
type ResultType = "header.size.sm" | "header.size.md" | "header.size.lg" |
"header.color.primary" | "header.color.secondary" | "header.nav.fontSize" |
"header.nav.align.left" | "header.nav.align.right" | "footer.caption" |
"footer.fixed.false" | "footer.fixed.true" | "footer.links.max.5" |
"footer.links.max.10" | "footer.links.nowrap.false" |
"footer.links.nowrap.true";
*/
Enter fullscreen mode Exit fullscreen mode

Nodes and Leaves

In many algorithms, it is necessary to distinguish the nodes of the tree from the leaves. If we consider those parameters that have object values as nodes, then we can apply the following generics:

type Nodes<T> = T extends object ? {
        [K in keyof T]: T[K] extends object ?
            (
                `${Exclude<K, symbol>}` |
                `${Exclude<K, symbol>}.${Nodes<T[K]>}`
            ) : never;
    }[keyof T] : never;

type Leaves<T> = T extends object ? {
    [K in keyof T]: `${Exclude<K, symbol>}${
            Leaves<T[K]> extends never ? "" : `.${Leaves<T[K]>}`
        }`
    }[keyof T] : never;

type InitialType = {
    header: {
        size: 'sm' | 'md' | 'lg';
        color: 'primary' | 'secondary';
        nav: {
            align: 'left' | 'right';
            fontSize: number;
        }
    };
    footer: {
        caption: string;
        fixed: boolean;
        links: {
            max: 5 | 10;
            nowrap: boolean;
        }
    }
};

type ResultNodes = Nodes<InitialType>;
type ResultLeaves = Leaves<InitialType>;
/*
type ResultNodes = "header" | "footer" | "header.nav" | "footer.links";
type ResultLeaves = "header.size" | "header.color" | "header.nav.align" |
"header.nav.fontSize" | "footer.fixed" | "footer.caption" |
"footer.links.max" | "footer.links.nowrap";
*/
Enter fullscreen mode Exit fullscreen mode

Conclusion

As you can see, Typescript has the necessary flexibility to simplify your code. And although tree structures seem to be quite complex, generics help to cope with them without types duplication. I hope that you have found something interesting in this article.

Enjoy your frontend development!

Top comments (0)