Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ES6 object cloning using spread operator is modifying input too

I have a fairly deep interface declared that looks something like this:

export interface Job {
    JobId: JobId; // type JobId = string
    UserId: UserId; // type UserId = string
    JobName: string;
    AudioFile: JobAudioFile; // this is an interface
    Status: JobStatus; // this is an enum
    Tracks: JobTracks[]; // 'JobTracks' is an enum
    Results: JobResults; // this is an interface
    Timestamps: JobTimestamps // interface
  }

Most of the members of this interface are themselves interfaces, with the general architecture following this pattern of using enums, strings, arrays and more interfaces. All code is written as TypeScript, transpiled down to JS and uploaded to AWS as JS. (Node 8.10 is running on AWS)

At one point in the code, I need to make a deep copy of a Job instantiation which was passed in as a function parameter:

export const StartPipeline: Handler = async (
  event: PipelineEvent
): Promise<PipelineEvent> => {
  console.log('StartPipeline Event: %o', event);

  const newBucket = await copyToJobsBucket$(event.Job);
  await deleteFromOriginalBucket$(event.Job);

  console.log(`Job [${event.Job.JobId}] moved to Jobs bucket: ${newBucket}`);

  event.Job.AudioFile.Bucket = newBucket;
  event.Job.Status = Types.JobStatus.Processing;

  // update the job status

  // VVV PROBLEM OCCURS HERE VVV
  const msg: Types.JobUpdatedMessage = new Types.JobUpdatedMessage({ Job: Object.assign({}, event.Job) }); 
  await Send.to$(event.Job.UserId, msg);

  return { ...event };
};

The definition of the JobUpdatedMessage:

  export class JobUpdatedMessage extends BaseMessage {
    constructor(payload: { Job: Types.Job }) {
      console.log('Incoming: %o', payload);
      const copy: object = { ...payload.Job };

      // VVV PROBLEM ON NEXT LINE VVV
      const filtered = JobUtils.FilterJobProperties(copy as Types.Job);

      super(MessageTypes.JobUpdated, filtered);
    }
  }

The problem is after the call to JobUtils.FilterJobProperties, payload.Job has also been mutated in an undesirable and unexpected way.

Here's the implementation of JobUtils.FilterJobProperties:

export const FilterJobProperties = (from: Types.Job): Types.Job => {
    const fieldsToRemove: string[] = [
      'Transcripts.GSTT',
      'Transcripts.WSTT',
      'Transcripts.ASTT',
      'TranscriptTracks',
      'Transcripts.Stream.File',
      'Transcripts.Stream.State',
      'AudioFile.Bucket',
      'AudioFile.S3Key',
    ];

    let job: Types.Job = { ...from }; // LINE ONE

    fieldsToRemove.forEach(field => _.unset(job, field));  // LINE TWO

    return job;
  };

(I'm using the lodash library here)

The line market 'LINE TWO' is also mutating the from function parameter, even though on 'LINE ONE' I'm doing what I think is a deep clone of from.

I know that this is the case because if I change 'LINE ONE' to:

// super hard core deep cloning
let job: Types.Job = JSON.parse(JSON.stringify(from));

... everything works as expected. from is not mutated, the resulting JobUpdatedMessage is as expected, and StartPipeline's event parameter doesn't have a bunch of properties removed from event.Job.

I struggled with hours on this, including relearning everything I believed I knew about cloning objects in Es6 using the spread operator.

Why was 'LINE ONE' mutating the input as well?

like image 816
John Dibling Avatar asked Oct 30 '25 21:10

John Dibling


1 Answers

Spread operator does shallow cloning same as Object.assign()

Shallow-cloning (excluding prototype) or merging of objects is now possible using a shorter syntax than Object.assign().

Spread operator

An example to understand spread operator and shallow cloning.

let obj = { 'a': { 'b' : 1 },'c': 2}

let copy = {...obj}

copy.c = 'changes only in copy'  //shallow-cloned 
copy.a.b = 'changed'             // still reference

console.log('original\n',obj)
console.log('\ncopy',copy)

Using spread operator object is shallow cloned so all the first level properties will become a copy while all the deeper level properties will still remain the references.

so as you see in example c property doesn't affect the original object since it is one first level depth, on the other hand b property changes affect the parent properties because it is at deep level and is still reference.

like image 66
Code Maniac Avatar answered Nov 02 '25 12:11

Code Maniac



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!