用于树结构的实用 TypeScript 泛型

发布: (2026年2月23日 GMT+8 18:03)
6 分钟阅读
原文: Dev.to

Source: Dev.to

有用的 TypeScript 泛型用于树结构的封面图

开发者被各种对象层次结构所包围,例如 DOM 树、React 组件树以及 NPM 依赖树。树形数据结构在代码中相当常见,可靠的 TypeScript 类型让使用它们更加自信。在本文中,我想与大家分享一些对我帮助最大的泛型。

Source:

深度部分

有时我们希望为第三方函数的所有参数(甚至是嵌套的参数)提供默认值,使它们全部变为可选的。在这种情况下,DeepPartial 类型非常有用:

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 泛型可以解决这个问题:

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

包含叶子节点

有时路径应当包含叶子节点,这时可以使用下面的泛型:

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

你可能会注意到 ResultType 中出现了奇怪的元素 header.nav.fontSize.${number}。这是因为 fontSize 参数可以接受无限数量的值。我们可以修改泛型以消除这类路径:

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

节点和叶子

在许多算法中,需要区分树的节点和叶子。如果我们把具有对象值的参数视为节点,那么可以使用以下泛型:

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;

(原文中 Leaves 的定义被截断。)

TypeScript 示例

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

结论

正如您所见,TypeScript 具备必要的灵活性来简化您的代码。虽然树结构看起来相当复杂,但泛型可以帮助您应对它们,而无需重复类型。希望您在本文中找到了一些有趣的内容。

祝您前端开发愉快!

0 浏览
Back to Blog

相关文章

阅读更多 »