原文链接:Useful TypeScript generics for tree structures
开发者在日常工作中经常会接触到对象的层级结构,例如 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 具备足够的灵活性来简化代码。尽管树形结构看似复杂,但通过泛型可以轻松应对,且无需重复定义类型。希望你能从本文中找到有用的内容。
祝你前端开发顺利!