Hey there, if you're diving into coding, you've probably run into all sorts of nested setups, like the way web pages are organized in the DOM, or how components nest in a React app, or even the web of dependencies in your package manager. These layered arrangements pop up everywhere in programming, and having solid TypeScript types can make you feel way more secure when tinkering with them. I'm excited to walk you through a few of my favorite generic helpers that have saved me tons of hassle.
Creating Flexible Nested Options
Picture this: you're dealing with a function from some library, and you want to set up defaults for every single input, including those buried deep inside objects, making everything optional. That's where a handy type like DeepPartial comes in super useful:
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;
};
*/
Generating All Possible Access Routes
Now, imagine you need to figure out every conceivable path through your nested data as a static type. A generic called Paths can handle that effortlessly:
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";
*/
But hey, there are times when you want those paths to focus just on the end points, or leaves. Here's a tweak to make that happen:
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 might spot something odd in the ResultType, like header.nav.fontSize.${number}. That pops up because fontSize could theoretically have endless variations. We can refine the generic to skip over those infinite cases:
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";
*/
Separating Branches from Endpoints
When you're building out logic that traverses these structures, it's often key to tell apart the internal points (nodes) from the final values (leaves). If we define nodes as spots holding object-like data, these generics can help you pinpoint them:
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";
*/
Wrapping It Up
Isn't it cool how TypeScript bends to fit your needs, streamlining your work without forcing you to repeat type definitions all over? Even though these nested setups can feel tricky at first, the right generics turn them into a breeze. I bet you'll spot a few ideas here to try in your own projects.
Have fun building those web wonders!