What not to do
When writing utilities to recursively clone objects that are passed through to a helper as transient args, you don't need to call a given top level function from within some deeply nested corner of that same function from within an if conditional that's within an if conditional nested within a for loop within a for loop...and so on. That dizzying approach becomes rapidly convoluted usually by the time you're approximately several nested layers of conditionals and/or for-loops deep.
Instead, utilize TypeScript and its generics and break it up into bite-sized pieces which have the added benefit of being reusable throughout your code base for type checking and more.
(1/4) Writing the Base Depth
type
This recursively mapped generic type traverses any given object and its key-value pairs using static types alone; it also poises us for straightforward type-inference in the following step
export type Depth<
Y extends { [record: string | symbol | number]: unknown },
X extends keyof Y = keyof Y
> = {
[H in keyof Y[X]]: Y[X][H][keyof Y[X][H]];
};
(2/4) Writing the InferDepth
generic type
The following generic gracefully "unwraps" Depth
by inferring each of the two generic types passed through.
export type InferDepth<T> = T extends Depth<infer U, infer X> ? U[X] : T;
The one-liner above may not look like it's doing much, but on ctrl+hover, TS Intellisense informs us that InferDepth
is of type
type InferDepth<T> = T extends Depth<infer U extends {
[record: string]: unknown;
[record: number]: unknown;
[record: symbol]: unknown;
}, infer X extends keyof infer U extends {
[record: string]: unknown;
[record: number]: unknown;
[record: symbol]: unknown;
}> ? U[X] : T
All of that from a one liner that "unwraps" Depth
by utilizing type inference
(3/4) Defining the inferObj
and objInference
tandem
Things are getting functional now, ja?
export const inferObj = <J, T extends InferDepth<J>>(props: T) => props;
export const objInference = <T extends Parameters<typeof inferObj>["0"]>(
props: T
) => inferObj(props);
Skeptical? That's okay, I was too while writing this code out for the first time late one night from scratch. But it works wonderfully, let's take a look at how with an example.
(4/4) To be non-readonly, or not to be
The following ugly object I slapped together contains one particularly bodacious nesting occurrence for the purpose of showcasing objInference
's utility
const devObject = {
theSimpleKey: "a simple string",
getDate: (rr: InstanceType<typeof Date>) => new Date(rr.toISOString()),
nestttt: () => [
{
"let's": {
keep: {
nesting: true,
deeper: [
{
because: {
why: {
the: {
fuck: { not: () => (Date.now() % 2 === 0 ? true : false) }
}
}
}
}
]
}
}
}
]
};
The deeply nested function returns a conditional within an array of objects that's also nested within an array of objects. We can expect the sample code to return true 500 times each second and false 500 times each second, but that's beside the point.
the point is, our objInference
function helper seamlessly handles the recursive cloning of this non-readonly object in a one liner -- and it doesn't sacrifice type-safety while also avoiding unnecessary complexity
const intelliCheck = () => objInference(devObj);
which has the following type inferred instantly
declare const intelliCheck: () => {
theSimpleKey: string;
getDate: (rr: InstanceType<typeof Date>) => Date;
nestttt: () => {
"let's": {
keep: {
nesting: boolean;
deeper: {
because: {
why: {
the: {
fuck: {
not: () => boolean;
};
};
};
};
}[];
};
};
}[];
};
Appending an as const
on the end of any given objects definition only heightens the type inference precision of these recursively cloning object rippers
const devObject = {
theSimpleKey: "a simple string",
getDate: (rr: InstanceType<typeof Date>) => new Date(rr.toISOString()),
nestttt: () => [
{
"let's": {
keep: {
nesting: true,
deeper: [
{
because: {
why: {
the: {
fuck: { not: () => (Date.now() % 2 === 0 ? true : false) }
}
}
}
}
]
}
}
}
]
} as const; // now it's readonly
Wrapping it up
To provide an insight into how this objInference
recursively cloning helper is actually ripping the internals of any given object passed through it, let's take a look at the following tamer object
const numObj = {
one: 1,
two: 2,
three: "3",
four: "IV"
} as const;
objInference(numObj)
while hovering and holding the "ctrl" key over the objInference cloner wrapping the numObj
constant above, its internal workings become evident
const objInference: <{
readonly one: 1;
readonly two: 2;
readonly three: "3";
readonly four: "IV";
}>(props: {
readonly one: 1;
readonly two: 2;
readonly three: "3";
readonly four: "IV";
}) => {
readonly one: 1;
readonly two: 2;
readonly three: "3";
readonly four: "IV";
}
Here's a link to the typescript playground containing pieces of the code used throughout this late-night post; it's worth checking out, especially if you're still apprehensive about this type-first approach to recursively cloning objects.
Cheers
Top comments (0)