0

I've built a sample using the Typescript playground, but the behaviour isn't related to typescript specifically. It's a bit easier to show in code (link includes compiled JS, for those looking to ignore TS):

If you extend a class which uses an indexed array as a property

class BaseClass {
  _meta: {[key: string]: string};
  constructor() {
    this._meta = {};
  }
}

class MyClass extends BaseClass {
  prop1?: string;

  constructor(init: Partial<MyClass>){
    super()
    Object.assign(this, init);
  }
}

Then from a base model use the reflection API to create other instances, you can get different behaviours depending how you build your subsequent models.

If you create a new object to assign to _meta, it works the way you'd expect it to:

const baseInstance = new MyClass({
  prop1: 'base',
  _meta: {
    hello: 'world'
  }
})

const reflectInstance = Reflect.construct(MyClass, [baseInstance]);
reflectInstance._meta = { hello: 'reflection' }

alert("1 " + JSON.stringify(baseInstance)) // as expected
alert("2 " + JSON.stringify(reflectInstance)) // as expected

but if you assign to the container using array notation, the scope becomes contaminated, i.e. it shares scope with the source model:

const contaminatedInstance = new MyClass({
  prop1: 'contaminated',
  _meta: {
    goodbye: 'world'
  }
})

const bogusInstance = Reflect.construct(MyClass, [contaminatedInstance]);
bogusInstance._meta['goodbye'] = 'contaminated';

alert("3 " + JSON.stringify(contaminatedInstance)) // _meta has unexpectedly changed
alert("4 " + JSON.stringify(bogusInstance)) // as a result of bogusInstance

Does anyone know why this is? I can fuzzily justify things by saying the _meta property has a common address, and because it's extended there wasn't a new invocation of the base model thus making it common; but this is a difficult case to remember when it comes up outside of unit testing; especially during a PR.

Any advice on how to avoid this while still using array notation would be great.

Thank you!

1 Answers1

1

That has nothing to do with Reflect. You'll get the same behaviour with:

  new MyClass(new MyClass({ shared: "false", _meta: { shared: "true" } })

It's just that Object.assign shallow copies, so the _meta property of both instances will contain a reference to the same object..

Some pseudo in-memory structure to make that more clear:

  #1 { // MyClass
    [[Construct]]: Code,
    prototype: #2
  }

  #2 { // MyClass.prototype
    constructor: #1,
    // yet empty
  }

  #3 { shared: "true" } // the _meta object, created inside the object passed to the first instance constructor

  #4 { // the object passed to the first instance constructor
    shared: "false",
    _meta: #3,
  }

  #5 { // the first MyClass instance
    [[prototype]]: #2,
    shared: "false", // shallow copied from #4
    _meta: #3
  }

  #6 { // the second MyClass instance
    [[prototype]]: 2,
    shared: "false", // shallow copied from #5
    _meta: #3, // same reference as #4._meta
  }
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151