Recursively Clone Objects without sacrificing Type Safety
Tags: typescript, oop, node, javascript
Published: 2023-06-01 09:38:49

## 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 

```ts
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.

```ts
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

```ts
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?

```ts
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

```ts
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

```ts
const intelliCheck = () => objInference(devObj);
```

which has the following type inferred instantly 

```ts
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

```ts
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

```ts
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

```ts
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](https://www.typescriptlang.org/play?target=99&jsx=1&ssl=57&ssc=14&pln=58&pc=13#code/C4TwDgpgBAIhbABYB4BQUoE0oQB7AgDsATAZygG8oBtAJwgGMB7W4gLilOFoEtCBzKAB9OIALYAjJgBthUQgFdJEWgF0OCwgGtCTAO6EoAXwA06KAA0c+ImShaIIJgDMsUALz3HLrKgB8HpTm1AASUHxeTq6Y1Baq6lixqqHJDlGJcSmqANyoRrmooJBQAJKEzipwCCgAKgGeNdYEJORVSMh8FbRQAKom4eUqlgEA-L1JUBw1BcyEXANdAPISAFaByABS-Y14zXZlXW0oG35+ABRgtExgpFMAlB4Bl9ekM0xzwFBMqwcqRAzQTzIHY2FpQAAKAENaJCxBACLRSMgihAfJ0VMsVn5qAAiAAMONU53MzxuU1QD3cAXRtExFyuNzuBVQLNZLNm82IEAAbpjGJ9PBRzEgIABlHhiMDSCAAaUcHBxkM4Eql0C4vAEOLMGH48JgkIIHDOtFoHDKXEhhABNXAEGRtp8+oIfkpAUIED0sANEGNtAAdMAmCVRYtRdw+Pwznc7tr5BAuMBE0bXTRzBghRhM1AcdLgAByUg4jgZrOZhzwYtp0sYd1cCMcbgKCCx6sYLnwFQcahV1tBXvViSMSEKUgQSv91t6RAgccT1si2dz1vOBQMLTF+RMYDJx5QM5OiB+3R6KNQACkUAATB53J48VAxo3oBxnJDpKOHkYe0vjN+J1+f1-JcAP7VQ-yA0sQIwECwKMKBIXIDlgDeD4BgIaRpB4ABhRBGC0DBPFPKkvh+QZ6CtH0uV5VZ+SZNlUCQ+QlExQIS3eMcoAARljYA9CYDhLx4xB6A4nEAGYtXMZwmAUU1sxKAA1HE8ngxD3i4ZlvhWX5yIBM5FDETE7iAA); it's worth checking out, especially if you're still apprehensive about this type-first approach to recursively cloning objects.

Cheers



![Boop](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eqtsgrdkidcgl5f3cr4n.gif)