microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.08k stars 12.49k forks source link

Computed Properties aren't bound correctly during Object/Class evaluation #27864

Open joeywatts opened 6 years ago

joeywatts commented 6 years ago

TypeScript Version: 3.2.0-dev.20181011

Search Terms: computed property expression

Code

const classes = [];
for (let i = 0; i <= 10; ++i) {
  classes.push(
    class A {
      [i] = "my property";
    }
  );
}
for (const clazz of classes) {
  console.log(Object.getOwnPropertyNames(new clazz()));
}

Expected behavior: The log statements indicate that each class in classes has a different property name (i should be evaluated at the time of the class evaluation and all instances of that class should have a property name corresponding to that evaluation of i).

Actual behavior: Compiled code logs:

[ '10' ] [ '10' ] [ '10' ] [ '10' ] [ '10' ] [ '10' ] [ '10' ] [ '10' ] [ '10' ] [ '10' ] [ '10' ]

Playground Link)%3B%0D%0A%7D)

mheiber commented 6 years ago

There's a type error on the line with the computed property:

A computed property name in a class property declaration must refer to an expression whose type is a literal type or a 'unique symbol' type.

However

DanielRosenwasser commented 6 years ago

Taking PRs and marking it as moderate (assuming you are familiar with spec-reading and the transform pipeline).

mheiber commented 6 years ago

What are the implications of getting rid of the type error? Will allowing arbitrary string and symbol values in computed field declarations affect inference?

Neuroboy23 commented 6 years ago

Type errors aside, here's one possible emit that works correctly. I haven't seen any other cases of invoking an IIFE with parameters like this. Are there any?

var classes = [];
for (var i = 0; i <= 10; ++i) {
    classes.push(
        function(i) {
            function A() {
                this[i] = "my property";
            }
            return A;
        }(i)
    );
}
mheiber commented 6 years ago

Maybe we can lean on the iife stuff that's already done to transpile let and const.

The currently generated code for Joey's example when targeting ES2015 is:

var _a, _b;
"use strict";
const classes = [];
for (let i = 0; i <= 10; ++i) {
    classes.push((_b = class A {
            constructor() {
                this[_a] = "my property";
            }
        },
        _a = i,
        _b));
}
for (const clazz of classes) {
    console.log(Object.getOwnPropertyNames(new clazz()));
}

If we move _a and _b into the containing scope of the class and use let then the output is correct:

"use strict";
const classes = [];
for (let i = 0; i <= 10; ++i) {
    let _a, _b; // <---- was moved
    classes.push((_b = class A {
            constructor() {
                this[_a] = "my property";
            }
        },
        _a = i,
        _b));
}
for (const clazz of classes) {
    console.log(Object.getOwnPropertyNames(new clazz()));
}

Update: would this be a big architectural change? If I understand correctly, lexicalEnvironmentStack is used in hoisting declarations, and is defined in transformer.ts, rather than in a particular transformer.

Kingwl commented 6 years ago

the special temporary variable and hoist is happened in ts transformer before es2015 perhaps we need a block level hoist?

mheiber commented 6 years ago

@Kingwl agreed. we're trying that approach in https://github.com/joeywatts/TypeScript/pull/15 used that approach in https://github.com/Microsoft/TypeScript/pull/28708

joeywatts commented 5 years ago

I noticed that there's also an issue with the binding of property declaration initializers in class expressions which are within an iteration statement. For example:

let arr = [];
for (let i = 0; i < 5; ++i) {
  arr.push(class C {
    test = i;
  })
}
arr.forEach(clazz => console.log(new clazz().test));

Since the i is block scoped within the loop, each class should capture its own value of i (causing it to print 0, 1, 2, 3, 4). The TypeScript compiler currently does not preserve this binding when targeting ES5 or lower. This currently produces no compile error and leads to different run time behavior when targeting ES2015 vs ES5.

I've investigated the changes required to the checker in order to pick up this binding, and it appears to me that a simple change to the checkNestedScopeBinding function to account for usage sites within instance property initializers (in addition to functions) would work. Should I open a separate issue for this?

xialvjun commented 3 years ago

I think code like this should be supported:

class A<P extends PropertyKey> {
    [p in P]: number;
}

But now it only support:

const m = Symbol('m');
const n = 'n' as 'n';

class A {
  [m]: number;
  [n]: string;
}