Search code examples
typescriptimmutabilitydeep-copy

Deep cloning a readonly instance


Lets say we have a class with a property that references another class. I want to be able to deep clone an "Immutable" (or Readonly) instance of it:

import * as _ from 'lodash'; 

interface IParent{
   name: string;
}

interface IChild{
   name: string;
   parent: IParent;
}

public class Child implements IChild{
   public name: string;
   public parent: string;

   constructor(props: IChild){
      Object.assign(this, props);
   }

   toImmutable(): Readonly<IChild> {
       return _.cloneDeep(this); //lodash deep clone
   }
}

While this code makes the first class properties on the child instance Readonly, the referenced object can still be edited:

let child = new Child({ 
   name: 'abc',
   parent: { name: 123 }
});

let immutable = child.toImmutable();
immutable.name = 'abc';  //not allowed
immutable.parent.name = 'abc';  //this is allowed!!!

Is there an elegant solution that would allow me to make EVERYTHING on the cloned object readonly?

Note: Looks like lodash has a method called cloneDeepWith that takes a "customizer"... Wondering if this might be going in the right direction.


Solution

  • The key is to create a custom type DeepReadonly<T> that you would use instead of Readonly<>:

    type DeepReadonly<T> = {
        readonly [K in keyof T]: DeepReadonly<T[K]>;
    }
    

    This type will recursively apply readonly attribute to all nested objects.

    See this in playground

    type DeepReadonly<T> = {
        readonly [K in keyof T]: DeepReadonly<T[K]>;
    }
    
    import * as _ from 'lodash'; 
    
    interface IParent{
       name: string;
    }
    
    interface IChild{
       name: string;
       parent: IParent;
    }
    
    class Child implements IChild{
       public name: string;
       public parent: IParent;
    
       constructor(props: IChild){
          Object.assign(this, props);
       }
    
       toImmutable(): DeepReadonly<IChild> {
           return _.cloneDeep(this); //lodash deep clone
       }
    }
    
    let child = new Child({ 
       name: 'abc',
       parent: { name: "123" }
    });
    
    let immutable = child.toImmutable();
    immutable.name = 'abc';  //not allowed
    immutable.parent.name = 'abc';  //not allowed