TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

JavaScript 'this.property' not recognised when assigned to with '='

Open goodeas opened this issue 3 years ago • 1 comments

VS Code Version: 1.73.0
TypeScript Version: 4.8.4
OS Version: Windows_NT x64 10.0.22621 (Windows 11)
Does this issue occur when all extensions are disabled?: Yes

Description

Normally, when a variable/property symbol is selected, that symbol and all other references to it in the same scope are highlighted.

In cases where an object's property has a value assigned to it with the = operator within a method, the property's symbol seems to lose its 'identity' and is no longer highlighted when other instances of that symbol in the same scope are selected. More importantly, related features such as Rename and Find all references also fail to recognise the symbol.

This issue describes the behaviour in VS Code. It was also tried in TS Playground with the same TS version and TS Config / Lang set to JavaScript, with slightly different results as indicated below.

Steps to reproduce

These examples can be reproduced with or without all extensions disabled.

Example 1 - one object

This is reproducible in VS Code. It works ok in TS Playground.

Paste this code into an empty JavaScript editor:

const o1 = {
  v: 1,
  returnV() { return this.v },
  incrementV() { this.v += 1 },
  assignV() { this.v = 2 },
};
o1.v = 2;

Select the v on line 2. All instances of 'v' are highlighted as expected, and hovering over any of those instances shows the following:

(property) v: number

Now, comment line 2:

  // v: 1,

then uncomment it again (or just undo):

  v: 1,

Select the v on line 2 again. Now, the instance of v in the assignV method is not highlighted, and hovering over it shows undefined as another possible type:

(property) v: number | undefined

Similarly, selecting the v in the assignV method does not highlight the other instances.

This behaviour seems to only happen where a value is assigned to this.v with the = operator. Note that o2.v, with the same assignment, is not affected. Also, selecting any this symbol still highlights all other this symbols.

A small edit in the body of the assignV method, like adding a space, seems to re-evaluate the symbol, and highlighting/hovering again works as expected.

Example 2 - using this of another object

This is reproducible in both VS Code and TS Playground.

This case is similar to Example 1, except this in the methods refers to another object. (This scenario is used in a real project.)

Paste this code into an empty Javascript editor:

const o2 = {
  v: 1,
};
const o3 = {
  /** @this o2 */
  returnV() { return this.v },
  /** @this o2 */
  incrementV() { this.v += 1 },
  /** @this o2 */
  assignV() { this.v = 2 },
};
o2.v = 2;

(The @this o2 JSDoc before each method tells those methods to use o2 as their this object. An alternative 'function type' JSDoc is @type {(this: typeof o2) => *}, but the behaviour is the same.)

Select the v in o2. All instances of v are highlighted except for the instance in the assignV method. Unlike Example 1, there doesn't seem to be a way to re-evaluate that symbol – it remains unrecognised as an instance of v.


The problem seems to be unique to the assignment of a value to a property of this with the = operator within an object method. It doesn't happen with other operators that change its value, like += or ++, or where this.property is only read, or when assigning with = outside of a method.

goodeas avatar Oct 30 '22 01:10 goodeas

Likely the same underlying cause as https://github.com/microsoft/TypeScript/issues/50240 - the assignment to this.v makes TS erroneously think it’s dealing with an ES5-style constructor. See also https://github.com/microsoft/TypeScript/issues/50338

fatcerberus avatar Oct 31 '22 18:10 fatcerberus

My original post was only about JavaScript in VS Code. Since @fatcerberus mentioned that the problem is likely due to the method being incorrectly interpreted as a constructor function, I wanted to understand the current behaviour more clearly.

The 'constructor function' issue explains some of the unexpected results, but not all. There are other differences between:

  • equivalent JavaScript and TypeScript code,
  • VS Code and Playground (with the same TS version), and
  • identical methods defined with short- and long-hand syntax.

Following is the results of testing various combinations of those.

Steps to reproduce

The objects in the following example code contain:

  • Three methods, defined in short syntax, which:
    • return a property value.
    • modify a property value.
    • assign a property value.
  • Three more methods, identical to the first three, but defined in long syntax, as property: function.

Copy the example code into:

  • a VS Code editor of the relevant file type (JavaScript or TypeScript).
  • the TS Playground, with TS Config / Lang set to JavaScript or TypeScript.

JavaScript

// Example 1.
const o1 = {
  v: 1,
  returnV() { return this.v },
  incrementV() { this.v += 1 },
  assignV() { this.v = 2 },
  
  returnVFunc: function () { return this.v },
  incrementVFunc: function () { this.v += 1 },
  assignVFunc: function () { this.v = 2 },
};
o1.v = 2;

// Example 2 - Using another object as `this`.
const o2 = {
  v: 1,
};
const o3 = {
  /** @this o2 */
  returnV() { return this.v },
  /** @this o2 */
  incrementV() { this.v += 1 },
  /** @this o2 */
  assignV() { this.v = 2 },
  
  /** @this o2 */
  returnVFunc: function () { return this.v },
  /** @this o2 */
  incrementVFunc: function () { this.v += 1 },
  /** @this o2 */
  assignVFunc: function () { this.v = 2 },
};
o2.v = 2;

TypeScript

// Use the JavaScript code above, but for Example 2,
// `this` is specified with type declarations rather than JSDoc,
// so replace the `o3` object with:
const o3 = {
  returnV(this: typeof o2) { return this.v },
  incrementV(this: typeof o2) { this.v += 1 },
  assignV(this: typeof o2) { this.v = 2 },
  
  returnVFunc: function (this: typeof o2) { return this.v },
  incrementVFunc: function (this: typeof o2) { this.v += 1 },
  assignVFunc: function (this: typeof o2) { this.v = 2 },
};

Expected results

Test Action Expected result
Highlighting of v Select the object's v property symbol. All other references to v should also be highlighted.
Type of this in methods Hover on any this symbol. Should show the object o1 (example 1) or o2 (example 2).
Type of v in methods Hover on any v symbol. Should show: (property) v: number
Return type of methods Hover on any method name. Should show the return type number (for return* methods) or void (for other methods).

Additionally, the equivalent methods declared in short and long syntax (such as assignV and assignVFunc) are expected to behave the same since they are semantically identical.

Observed/unexpected results

Most of the problems occur with the assignV and assignVFunc methods, where a value is assigned to this.v with the = operator. Results for those two methods are displayed separately, as they should be identical, but aren't.

The return* and increment* functions do not have any problems (except in VS Code / TypeScript).

JavaScript - Example 1

Test Expect VS Code
JavaScript
TS 4.8.4
Playground
JavaScript
TS 4.8.4
Playground
JavaScript
TS Nightly
assignV()
Highlighting of v Highlighted Not highlighted (sometimes*)
Type of this o1
Type of v number number|undefined
Return type void
assignVFunc()
Highlighting of v Highlighted Not highlighted Not highlighted Not highlighted
Type of this o1 this this
Type of v v: number assignVFunc.v: any assignVFunc.v: any assignVFunc.v: number
Return type void typeof assignVFunc typeof assignVFunc typeof assignVFunc

* for example, after commenting then uncommenting the o1.v property. See original post.

JavaScript - Example 2

Test Expect VS Code
JavaScript
TS 4.8.4
Playground
JavaScript
TS 4.8.4
Playground
JavaScript
TS Nightly
assignV()
Highlighting of v Highlighted Not highlighted Not highlighted Not highlighted
Type of this o2
Type of v number number|undefined
Return type void
assignVFunc()
Highlighting of v Highlighted Not highlighted Not highlighted Not highlighted
Type of this o2
Type of v v: number assignVFunc.v: number assignVFunc.v: number assignVFunc.v: number
Return type void typeof assignVFunc typeof assignVFunc typeof assignVFunc

TypeScript - Example 1

Test Expect VS Code
TypeScript
TS 4.8.4
Playground
TypeScript
TS 4.8.4
All methods
Highlighting of v Highlighted Not highlighted
Type of this o1 any
Type of v number any
Return type number or void any instead of number

TypeScript - Example 2

Test Expected VS Code
TypeScript
TS 4.8.4
Playground
TypeScript
TS 4.8.4
All methods
Highlighting of v Highlighted
Type of this o2
Type of v number
Return type number or void

Summary

The problem is mostly with JavaScript. TypeScript behaves ok except for in VS Code / TS 4.8.4.

Many of the observations point at the incorrect 'constructor function' interpretion as being the main cause. The TypeScript JSDoc documentation says:

  • For the @constructor JSDoc: "The compiler infers constructor functions based on this-property assignments..."
  • For the @this JSDoc: "The compiler can usually figure out the type of this when it has some context to work with...".

I can't think why a constructor function would be defined on an object property, but since JavaScript allows that, I'm thinking that a JavaScript function (or at least an object method) should never be interpreted as a constructor function unless it is explicitly preceded by a @constructor JSDoc. Is that reasonable? The different behaviour between the identical short- and long-hand methods suggests that the TS engine is currently making assumptions from syntax or scope that don't match the intended context.

goodeas avatar Nov 13 '22 00:11 goodeas

I'm thinking that a JavaScript function (or at least an object method) should never be interpreted as a constructor function unless it is explicitly preceded by a @constructor JSDoc. Is that reasonable?

The intent of JS inference is to, well, infer. These are heuristics and by definition can have false positives and negatives, but the ones in place here are pretty good in terms of detecting class vs not-class, and people have taken pretty strong dependencies on this behavior. It's always possible to annotate your way out of heuristic misdetections, and we need to place that burden on the rare cases, note the common ones.

RyanCavanaugh avatar Dec 02 '22 18:12 RyanCavanaugh

@RyanCavanaugh I accept that inference won't always give the expected result, but you also said:

It's always possible to annotate your way out of heuristic misdetections

Unfortunately that is not true in this case, and this was the primary reason for posting this issue. Annotating assignV() with @this o2 (in JavaScript Example 2 above) should indicate that it is a method rather than a constructor function. It seems to partially work – the types of this and v are then interpreted correctly, but the highlighting of v (and renaming, etc) still doesn't work for some reason. Is there something that can be done to address that?

The secondary reason for this issue was that what works in TS Playground doesn't work in VS Code with the same TS version (see the tables above). Maybe that's a separate issue.

goodeas avatar Dec 03 '22 01:12 goodeas