Skip to content

In JS, noImplicitThis puts an error on this inside a constructor function #25979

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

Closed
jameskeane opened this issue Jul 26, 2018 · 10 comments
Closed
Labels
checkJs Relates to checking JavaScript using TypeScript Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@jameskeane
Copy link
Contributor

TypeScript Version: 30c4149

Search Terms:
jsdoc @constructor this
Code

/** @constructor */ function Example() { this.m = 1; }
/** @constructor */ var Example2 = function() { this.j = 2; };

Compiled with --allowJs --checkJs --noImplicitThis

Expected behavior:
Compiles fine.

Actual behavior:

error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.

Playground Link:
https://www.typescriptlang.org/play/#src=%2F**%20%40constructor%20*%2F%20function%20Example()%20%7B%20this.m%20%3D%201%3B%20%7D%0D%0A%2F**%20%40constructor%20*%2F%20var%20Example2%20%3D%20function()%20%7B%20this.j%20%3D%202%3B%20%7D%3B%0D%0A

@sandersn sandersn changed the title In JS, this not properly inferred in @constructor functions. In JS, noImplicitThis puts an error on this inside a constructor function Jul 26, 2018
@sandersn
Copy link
Member

Currently this is working as intended. I think we should keep it that way until we find out more about other scenarios we might have missed, and how popular these scenarios are.

The intent of --noImplicitThis is to restrict usage of this to classes and select functions where some thought has gone into usage. Constructor functions are not meant to be the default when --noImplicitThis is turned on. So, in JS and TS, --noImplicitThis marks this in constructor-functions with an error unless there is an explicit (or contextual) this type. [1] The way to specify this in Javascript is with the @this jsdoc tag.

I don't expect many pure JS projects to turn on --noImplicitThis since it produces a weird mixture of loose-JS and strict-TS semantics. If somebody does choose to turn it on (as webpack did a few weeks ago), I expect it to be a signal that constructor functions are not to be used except in legacy code. , and that the legacy code needs either @this jsdoc or @ts-ignore comments.

Maybe we're missing some scenario, though. Do you have a mixed TS/JS codebase? If it's a mixed codebase, is it mostly JS? Are you turning on --noImplicitThis specifically or just --strict? If --strict, are you inundated with no-implicit-any errors from the JS code as well?

[1] Note that both JS and TS correctly infer the type of Example and Example2:

var Example2 = function() { this.j = 2 }
var e2 = new Example2()
e2.j // <-- e2.j: number

Despite knowing the type of this, both JS and TS give you an error with --noImplicitThis is turned on.

@sandersn sandersn added the Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature label Jul 26, 2018
@jameskeane
Copy link
Contributor Author

jameskeane commented Jul 26, 2018

I'm trying to switch to tsc from closure-compiler for a large jsdoc typed codebase. An example of a large codebase in this style is the closure-library. [I'm trying to do this for a couple reasons, but the biggest is that closure-compiler does not have string literal types and can not specialize the return type of document.createElement or require.]

The intent of --noImplicitThis is to restrict usage of this to classes

Yes, and if you tag a function with the jsdoc @constructor or @class tag, you are telling the type system that you're defining a class.

Maybe a better example to consider:

/** @constructor */
function Example() {
  // this _should_ throw an error, since Example#method takes a string but doesn't because `this` is any.
  this.method(1);
}

/**
  * @param {string} a
  */
Example.prototype.method = function(a) {
  // tsc correctly infers `this` to `Example`.
};

With my PR (#25980), this will correctly throw a error TS2345: Argument of type '1' is not assignable to parameter of type 'string'.

Could you clarify what you mean by "contextual type"?

@jameskeane
Copy link
Contributor Author

To be clear I'm only talking about constructor functions that are tagged with the jsdoc class tag. If not tagged, I agree that this should be any

@sandersn
Copy link
Member

Thanks. I see the value in typing this inside a constructor function, although I'm still a bit worried about things going circular since the type of this comes partly from the function itself. That is, inside the constructor this.x = 1 is going to check this.x based on the type of this.x = 1. If it passes the tests and user tests (jake runtests-parallel ru=user), it's probably fine, though.

After some discussion with @mhegazy we decided that @constructor should make the error go away in addition to typing this, just like @this {Example} would. The compiler knows the type (barring the circularity concerns). We considered whether to infer this for all Javascript functions, but it's ambiguous in the object-literal-method vs class-method vs nested-class case:

// object literal "singleton" with methods
var o = {
  state: [1,2,3]
}
o.method = function (x) {
  this.state.push(x) // this.state should be visible here
}
o.method(4)
// exports with nested class
var ok = {}
ok.top = function () { }
ok.nestedClass = function (name) {
    this.name = name
    // this.top shouldn't be visible here!
}
new ok.nestedClass()
module.exports = ok

// class with nested class
function f() {
  this.state = [1,2,3]
}
f.nestedClass = function (name) {
    // this.state shouldn't be visible
    this.name = name // this.name should be a fresh property
}
module.exports = f

//
f.staticMethod = function (name) {
    // this.state shouldn't be visible
    this.name = name // this.name should be the Function.name of `f`
}
new f.nestedClass() // fine!
f.staticMethod() // `this.name = name` tries to overwrite the name of `f` and fails silently

We could try to come up with heuristics to distinguish these 4 cases, but it would be better to require @constructor (or @this) for the nested-class cases. In fact we get the wrong this in the o.nestedClass and f.nestedClass cases right now (#25906).

I would like to see tests of three things:

  1. Dual function/new-callable constructor functions. These don't necessarily need to work when marked with @constructor, I just want a test of how they behave:
/** @class */
function C() {
  if (!(this instanceof C)) {
    return new C()
  }
  this.x = 1
}

(I think this is the pattern, but I just wrote it off the top of my head, so it could easily be wrong.)

  1. Possibly circular initialisers:
/** @class */
function C() {
  // see the explanation of contextual typing below.
  this.functions = [x => x, x => x + 1, x => x - 1]
}

The compiler might have trouble figuring out the type of this.functions because it wants to needs to know the type of the xs, and they want to know the type of their context, which includes this.functions, so the compiler needs to know the type of its initialiser, etc etc.

  1. Also, this should only work in Javascript.

Re: contextual typing
In your example,

Example.prototype.method = function (a) { this.x }
  1. this is contextually typed by Example because
  2. It's inside function (a) {}, which is contextually typed by Example.prototype.method because
  3. it's the right-hand-side of a special JS prototype-property assignment.

@jameskeane
Copy link
Contributor Author

@sandersn should we continue discussion in PR #25980?

Re:

  • [1] This pattern is already tested in /tests/cases/conformance/salsa/constructorFunctions.ts.
  • [2] I'll add the circular initializer case to constructorFunctions.ts
  • [3] The way that the PR is written, calling getJavaScriptClassType which calls isJavaScriptConstructor which only infers the constructor type if in JS. It would still return the assigned type from getAssignedClassType, but I'd need some guidance on if that is acceptable.

@jameskeane
Copy link
Contributor Author

jameskeane commented Jul 27, 2018

Would we need to throw a TS2348 (error TS2348: Value of type 'typeof xx' is not callable. Did you mean to include 'new'?) if trying to call a function tagged @class?

Is this what you meant when you said in [1]: "These don't necessarily need to work when marked with @constructor"?

Otherwise this would type-check fine, but error at runtime:

/** @constructor */
function Example5() {
  this.method();
}
Example5.prototype.method = function() {}
Example5();

@sandersn
Copy link
Member

Well, what I meant was that it’s acceptable to have type errors inside the callable-constructor function when tagged with @class. However, the error is a good idea, although certainly optional for the PR.

@mhegazy mhegazy added Suggestion An idea for TypeScript Fixed A PR has been merged for this issue checkJs Relates to checking JavaScript using TypeScript and removed Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Jul 31, 2018
@mhegazy mhegazy added this to the TypeScript 3.1 milestone Jul 31, 2018
@mhegazy
Copy link
Contributor

mhegazy commented Jul 31, 2018

@sandersn
Copy link
Member

sandersn commented Aug 1, 2018

I think the correct update is JSDoc-support-in-Javascript, since that enumerates our tag support. In fact, it currently uses @constructor as an example of an unsupported tag (!).

I'll write up a description with the new semantics and send out a PR.

@sandersn
Copy link
Member

sandersn commented Aug 6, 2018

OK, the handbook is updated, and I also updated the wiki until we can publish a new version of the handbook. Afterwards I'll remove the text from the wiki and point to the handbook.

sandersn added a commit that referenced this issue Jul 6, 2020
Previously,  `this: this` in constructor functions only when there was
an explicit `@constructor` tag on the function. Now, `this: this` for
any function that's known to be a constructor function.

This improves completions inside constructor functions; also note that
previously the compiler *did* type `this: this` inside methods of constructor
functions, so this fix makes us more consistent. This is reflected in
the large number of baselines that improve.

The fix is a simple switch to `isJSConstructor`, which is the standard
way to detect constructor functions. I'm not sure why the original PR
didn't use this method.

I remember discussing this limitation in the original bug, #25979, and
I guess I decided that it made sense. But I was heavily primed by the bug's
framing of the problem in terms of `noImplicitThis`, which *should*
require an explicit `@constructor` tag.

With better typing comes better detection of `@readonly` assignment; I
had to fix the readonly detection code to use `isJSConstructor` as well.
sandersn added a commit that referenced this issue Jul 8, 2020
* Type `this` in more constructor functions

Previously,  `this: this` in constructor functions only when there was
an explicit `@constructor` tag on the function. Now, `this: this` for
any function that's known to be a constructor function.

This improves completions inside constructor functions; also note that
previously the compiler *did* type `this: this` inside methods of constructor
functions, so this fix makes us more consistent. This is reflected in
the large number of baselines that improve.

The fix is a simple switch to `isJSConstructor`, which is the standard
way to detect constructor functions. I'm not sure why the original PR
didn't use this method.

I remember discussing this limitation in the original bug, #25979, and
I guess I decided that it made sense. But I was heavily primed by the bug's
framing of the problem in terms of `noImplicitThis`, which *should*
require an explicit `@constructor` tag.

With better typing comes better detection of `@readonly` assignment; I
had to fix the readonly detection code to use `isJSConstructor` as well.

* Remove `Add @Class tag` fix for noImplicitThis.

The new rules mean that it never applies. It's possible that it should
apply to functions like

```js
function f() {
  this.init()
}
```

In which `init` is never defined, but I think this program is incomplete
enough that not offering the fix is fine.

* Fix precedence of `@this`

Previously, both `@class` and `@this` in a jsdoc would cause the `@this`
annotation to be ignored. This became a worse problem with this PR,
because `this` is correctly typed even without the annotation.

This commit makes sure that `@this` is checked first and used if
present.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
checkJs Relates to checking JavaScript using TypeScript Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants