javascripttypescriptthisgetterdefineproperty

TypeScript bug? `this` difference between `get` syntax and `defineProperty`'s `get`


I have got this code:

var x = {
  x1: 5,
  x2: 7
};

var y = {
  ...x,
  _originalX2: x.x2,
  get x2() {
    console.log(this.x1);
    return 9;
  }
};
console.log(y.x2);

var z = {
  ...x,
  _originalX2: x.x2
};
Object.defineProperty(z, 'x2', {
  get: function() {
    console.log(this.x1);
    return 9;
  }
})
console.log(z.x2);

When I run this as JavaScript in the browser or in NodeJS, I get the output:

5
9
5
9

When I run the same code as TypeScript (see https://repl.it/repls/TornHomelyThing), I get the output:

undefined
9
5
9

I also see what JS TS generates from it @ https://www.typescriptlang.org/play/?ssl=1&ssc=1&pln=22&pc=1#code/G4QwTgBAHhC8EG8oEYBcECsAaaAmdA7AL4DcAUGaJAJ5yJkQQB0LUODEA+gPZgCWAcz4A7EABsAGvihMoudowEBTAC54AFAEoEHRgGNuwgM7cxSpmO4D1KgBZ8js5JvKNGYVQFcwwiAE5XCCIyUjIDY1NzS2tqWVwXCioIAC86HUYWWQUuXkERcSlUGTkQ8gB5ACMAKyU9FSYAEyUAMxElAAUwbgAHJTAVanVknAByORGcBAhlFXRmz2E6vkMtejcIcJMzCysbe0cUBPWPFW9fAI4iIM0wwy2o3eS4hKA .

My question is two-fold:

  1. Is this to be considered a bug in TypeScript given that the same code in JS has different behavior?
  2. Is there a good reason for the this not being able to reference the x1 in the TypeScript generated JS? Or, is that a bug in JS itself?

Solution

  • Is this to be considered a bug in TypeScript given that the same code in JS has different behavior?

    Yes, this definitely looks like a bug related to Typescript's transpilation of object spread syntax. The compiled TS is not doing the same thing as the JS.

    In the first, you define a y object with a property x2, which is a getter:

    var y = {
      ...x, 
      _originalX2:x.x2, 
      get x2(){
    

    So referencing y.x2 later results in the getter being invoked. But in the compiled Typescript, we get:

    var x = { x1: 5, x2: 7 };
    var y = Object.assign(
      Object.assign({}, x),
      {
        _originalX2: x.x2,
        get x2() {
          console.log(this.x1);
        }
      }
    );

    This will result in the getter being invoked immediately - getters get invoked when in an object passed as the 2nd or more argument inside Object.assign. And when the getter is invoked, this refers to the second argument, this object here:

    {
      _originalX2: x.x2,
      get x2() {
        console.log(this.x1);
      }
    }
    

    Which does not have an x1 property, thus it logs undefined.

    The undefined is logged before the Object.assign finishes, before you get to the next line of console.log(y.x2);.

    For a more minimal example:

    var y = {
      ...{}, 
      get prop(){
        console.log('should not be invoked immediately');
      }
    };
    

    gets incorrectly transpiled to

    var y = Object.assign({}, { get prop() {
            console.log('should not be invoked immediately');
        } });
    

    If you make it so that Typescript doesn't transpile object spread syntax, by bumping the target past ES2017, it'll work. For example, with ESNext, the above gets transpiled to:

    var y = {
        ...{},
        get prop() {
            console.log('should not be invoked immediately');
        }
    };