【翻译】实用的 TypeScript 树形结构泛型

17 阅读2分钟

原文链接:Useful TypeScript generics for tree structures

作者:Marat Sabitov

开发者在日常工作中经常会接触到对象的层级结构,例如 DOM 树、React 组件树和 NPM 依赖树。树形数据结构在代码中十分常见,而可靠的 TypeScript 类型定义能让开发者在处理这类结构时更有信心。本文将分享几个对我帮助极大的树形结构相关泛型。

深度可选类型(DeepPartial)

有时我们需要为第三方函数的所有参数(甚至嵌套参数)提供默认值,让这些参数都变为可选。这种情况下,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)

另一种常见场景是,需要将树形结构中所有可用路径推断为类型。Paths 泛型可以解决这个问题:

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

有时路径需要包含叶子节点(即属性的具体值),这时可以使用以下泛型:

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

你可能会注意到 ResultType 中出现了一个特殊的类型 header.nav.fontSize.${number}。这是因为 fontSize 参数可以取无限多个数值。我们可以修改泛型来剔除这类路径:

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

节点与叶子节点类型(Nodes and Leaves)

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

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

总结

由此可见,TypeScript 具备足够的灵活性来简化代码。尽管树形结构看似复杂,但通过泛型可以轻松应对,且无需重复定义类型。希望你能从本文中找到有用的内容。

祝你前端开发顺利!