用于树结构的实用 TypeScript 泛型
Source: Dev.to

开发者被各种对象层次结构所包围,例如 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 具备必要的灵活性来简化您的代码。虽然树结构看起来相当复杂,但泛型可以帮助您应对它们,而无需重复类型。希望您在本文中找到了一些有趣的内容。
祝您前端开发愉快!