Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
joeywatts opened this issue Oct 12, 2018 · 9 comments
Open
Labels
Bug A bug in TypeScript Domain: Transforms Relates to the public transform API Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Help Wanted You can do this
Milestone

Comments

@joeywatts
Copy link
Contributor

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

@mheiber
Copy link
Contributor

mheiber commented Oct 12, 2018

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

  • It's still possible to produce correct emit in this case, even though there's a type error (if that's desirable)
  • The class fields proposal allows code like in the example. Chrome Canary implements the spec and produces the expected output for the example code.

@ghost ghost added Bug A bug in TypeScript Domain: Transforms Relates to the public transform API labels Oct 12, 2018
@DanielRosenwasser DanielRosenwasser added Help Wanted You can do this Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". labels Oct 12, 2018
@DanielRosenwasser
Copy link
Member

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

@mheiber
Copy link
Contributor

mheiber commented Oct 13, 2018

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
Copy link

Neuroboy23 commented Oct 16, 2018

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
Copy link
Contributor

mheiber commented Oct 18, 2018

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
Copy link
Contributor

Kingwl commented Oct 23, 2018

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

@DanielRosenwasser DanielRosenwasser added this to the TypeScript 3.2 milestone Oct 23, 2018
joeywatts pushed a commit to joeywatts/TypeScript that referenced this issue Oct 26, 2018
Signed-off-by: Joseph Watts <jwatts43@bloomberg.net>
@mheiber
Copy link
Contributor

mheiber commented Oct 26, 2018

@Kingwl agreed. we're trying that approach in joeywatts#15
used that approach in #28708

joeywatts pushed a commit to joeywatts/TypeScript that referenced this issue Nov 15, 2018
Signed-off-by: Joseph Watts <jwatts43@bloomberg.net>
joeywatts pushed a commit to joeywatts/TypeScript that referenced this issue Nov 21, 2018
Signed-off-by: Joseph Watts <jwatts43@bloomberg.net>
joeywatts pushed a commit to joeywatts/TypeScript that referenced this issue Nov 27, 2018
Signed-off-by: Joseph Watts <jwatts43@bloomberg.net>
joeywatts added a commit to bloomberg/TypeScript that referenced this issue Nov 27, 2018
* Fix microsoft#27864

Signed-off-by: Joseph Watts <jwatts43@bloomberg.net>

* Tests for computed field scope fix

Signed-off-by: Joseph Watts <jwatts43@bloomberg.net>
@joeywatts
Copy link
Contributor Author

joeywatts commented Nov 29, 2018

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

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Domain: Transforms Relates to the public transform API Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Help Wanted You can do this
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants