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> = 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";
*/
Sometimes path should include leaves, so you can use the next generic:
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";
*/
You may notice a strange element header.nav.fontSize.${number} in the ResultType. It appears due to the fact that the fontSize parameter can take an infinite number of values. We can modify the generic to eliminate such paths:
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
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> = 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";
*/
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!
Top comments (0)