Useful TypeScript generics for tree structures

Published: (February 23, 2026 at 05:03 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

Cover image for 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;
};
*/

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, Exclude extends string = ''> = T extends object
    ? {
          [K in keyof T]: `${Exclude}${'' | Paths<T[K], \`\${Exclude}\${K & string}\`> extends '' ? '' : \`.${Paths<T[K], \`\${Exclude}\${K & string}\`>}\`}`;
      }[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";
*/

Including leaves

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

type Paths<T, Exclude extends string = ''> = T extends object
    ? {
          [K in keyof T]: `${Exclude}${'' | Paths<T[K], \`\${Exclude}\${K & string}\`> extends '' ? '' : \`.${Paths<T[K], \`\${Exclude}\${K & string}\`>}\`}`;
      }[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";
*/

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

type Paths<T, Exclude extends string = ''> = T extends object
    ? {
          [K in keyof T]: `${Exclude}${'' | Paths<T[K], \`\${Exclude}\${K & string}\`> extends '' ? '' : \`.${Paths<T[K], \`\${Exclude}\${K & string}\`>}\`}`;
      }[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";
*/

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, Exclude extends string = ''> = T extends object
    ? {
          [K in keyof T]: T[K] extends object
              ? `${Exclude}${K & string}` | `${Exclude}${K & string}.${Nodes<T[K], \`\${Exclude}\${K & string}\`>}`
              : never;
      }[keyof T]
    : never;

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

(The Leaves definition is truncated in the original source.)

TypeScript Example

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";
*/

Fullscreen Controls (illustrative)

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!

0 views
Back to Blog

Related posts

Read more »