Typescript: Flag for strict default function `this` types for call-site and assignability checking (`--strictThis`)

Created on 8 Apr 2016  ·  5Comments  ·  Source: microsoft/TypeScript

6018 was originally going to add a --strictThis flag that made function this types default to void or the enclosing class (instead of any) for purposes of call-site and assignability checking, but that functionality was dropped (details). This is a follow-up suggestion for the dropped functionality, whatever the flag ends up being named.

Add a Flag Awaiting More Feedback Suggestion

Most helpful comment

I just ran into a problem where the obvious solution would be that methods without an explicit this parameter default it to the this type. Searching brought me here. So the main problem is... performance? In any case, I hope a --strictThis option makes it into TS sometime (adding my 👍 ).

All 5 comments

Please see https://github.com/Microsoft/TypeScript/issues/7689 for the relevant desing discussion

I just ran into a problem where the obvious solution would be that methods without an explicit this parameter default it to the this type. Searching brought me here. So the main problem is... performance? In any case, I hope a --strictThis option makes it into TS sometime (adding my 👍 ).

Maybe we can revive this thing as I believe it would bring a lot of benefit.

Regarding the "backwards compatibility" problem - is this due to class-this context for methods which in reality should stay any because they are allowed to be called from different contexts? If yes, --strictThis could only implicitly set the context to class-this for methods which have a typescript implementation and actually use this in the method body. DefinitelyTyped could adapt step by step and pure typescript code bases could use the improved type check right away.

I also hope this is added back. I just started using Typescript last week, and this is basically the only "type error" I've run into (and repeatedly for that matter) in my all Typescript codebase. I hit it frequently, as I am using an ES6 classes as a router/controller in my Express app, e.g. app.use(this.someClassFunction). When I see failing tests with an error like cannot access property someClassFunction of undefined, I just change the declaration syntax to ES7 function bind syntax class X { constructor() {} x: (req, res) => {} } and the issue is fixed, but it would be really nice to know about that at compile time instead of at runtime.

I've been impressed with Typescript in general but was very surprised to hear that it's not checked at compile time since I can't think of another typed language, where such an error is possible.

Hi, I was surprised that strict type checking for the this parameter is currently that limited and would like to see enhancements in future versions. Since the problem caused by the absence of type checking made it into the FAQ, it seems to be supising for many users.

As already mentioned in previous comments, the issues #3694 and #6018 have already proposed some changes and some of them were already implemented in the pull requests #4910 and #6739. Since there is a long history, I found it difficult to get an overview about the situation. I therefore tried to summarize it below. I hope that I did not mix up anything. At the bottom, I bring up my own thoughts about the current situation.

Implemented


:heavy_check_mark: Polymorphic this type

Proposed by #3694 and implemented by #4910. It has added a special type which represents the actual type of this which might be a subclass.

class C {
  newInstance(): this {
    // Error because 'this' my be a subclass.
    return new C();
  }
}

class D extends C {
  run() {
    // Works
    let x: D = this.newInstance();
  }
}

Within method bodies, this now has type this (as default).

class C {
  f() {
    // `this` and therefore `x` have type `this`. In previous versions, they
    // would have type `C`.
    let x = this;
  }
}

// However, for the users of the method, `this` is still considered to has
// type `any`. Even so it might cause a runtime error, the following compiles
// because `null` is assignable to `any`.
const c = new C();
c.f.call(null);

Note that TypeScript looses the “it might be a subtype” semantic when you are outside of the class.

class C {
  f(arg: this) {
    // Error because the method might require a subtype of C.
    this.f(new C()); // <-- Error
  }
}

declare let c: C;
// The method might still require a subtype of C but TypeScript does not
// have this kind of semantic outside of the class.
c.f(new C()); // <-- Compiles



:heavy_check_mark: Type specifications for this parameter

Proposed by #3694, refined by #6018, and implemented by #6739. There were proposals for different syntaxes but I only show the implemented one.

function f(this: string) {
  // A function that expects a string for the `this` parameter.
}

Note that this also works when a function is called as constructor. It then also specifies the type of the created object.

interface F {}
function F(this: F) {}
let f = new F(); // `f` has type `F`



:heavy_check_mark: Type checking for this parameter on call sites

Proposed by #3694, resumed by #6018, and implemented by #6739. It's about actually checking the argument for the this parameter.

declare function f(this: string): any;
const obj = {f};
obj.f(); // <-- Error, `obj` is not a `string`

Note that #3694 alternatively suggested that we could already fail when a function is assigned to an object that does not satisfy the this parameter. Then, the second line of the code snipped would already fail. However, it was never mentioned again (and I think it would be strange).



:heavy_check_mark: --noImplicitThis

Implemented in #6739 as replacement for --strictThis which will be described later. The --noImplicitThis causes a compilation error when you access this but the type of this is implicitly set to any. This can happen when you access this in the function body or when you use new.

function f() {
  this.x; // <-- Error because type of this is unspecified.
}

function C() {}
const c = new C(); // Error because type for `this` in `C` unspecified.

However, note that it does not check call sites of such methods. Since the default type for this is inconsistent inside and outside of the method body, the following still compiles without errors.

class C {
  f() {
    // Fine because `this` has type `this`.
    this.f();
  }
  g() {}
}

const c = new C();
// Fine because `C.f` has type `(this: any) => void`.
c.f.call(null);



:heavy_check_mark: Modified assignability checks

Proposed by #6018 and implemented by #6739. It allows assigning anything to the this parameter if the this parameter is void. Usually, only undefined (or null) can be assigned to void.

function f(this: void): void {}
const obj = {f};

// Passes `obj` to `void` but still compiles.
obj.f();
// Assigns `string` to `void` but still compiles.
const g: (this: string) => void = f;



:heavy_check_mark: Contextually typed this

Proposed by #6018 and implemented by #6739. In some cases, TypeScript will infer the type for this from the context. For example, when you directly assign a new function to a variable that has a declared type. The inferred type will only be used in the method body but not on call sites.

declare let x: (this: string) => void;
x = function () {
  // `this` has type `string`.
};

If the type for this is not specified by the target, this is contextually typed to the object where it is assigned to.

const obj = {
  x: 0,
  f: function () {
    // Ok, `this` is contextually typed as `typeof obj`.
    this.x;
  },
}

Proposed


:blue_book: New implicit type for this parameter (--strictThis)

Proposed by a comment in #3694 and further specified by #6018 but not yet implemented. The proposal is about adding a new compiler option like --strictThis which changes the default type for this from any to either this or void.

class C {
  f() {
    // Works since `this` is of type `this`, just as in current versions.
    this.g();
  }
  g() {}
}

const c = new C();
// Fails because `C.f` is of type `(this: C) => void`.
// Currently, TypeScript considers the type to be `(this: any) => void`.
c.f.call(null);

The type shall be this for method-style declarations and void for function-style declarations. Currently, it is always this in the method body, and any outside of the method body.

// type: (this: void) => void
function f() {}

interface I {
  // type: (this: void) => any
  g: () => any;
  // type: (this: this) => void
  h();
}

const obj = {
  // type: (this: typeof obj) => void
  // (However, I think it should be `(this: void) => void` as I describe later)
  g: function() {},
  // type: (this: typeof obj) => void
  h(): void {},
}

According to #7689, it has not been implemented because

  • it decreases performance by 5% to 10%, and
  • the difference for method-style declarations and function-style declarations might not be obvious for some users. (As a user that does not have that much experience with TypeScript, I want to mention that I kind of expected this difference until I found out that it does not exist.)

My thoughts


:large_blue_diamond: Safe method invocation still missing

A lot of stuff have already been implemented, but we are still kind of missing safe method invocations. For me, safe method invocations actually look like one of the most important motivations behind the topic. As of today, we only get safe method invocations if we always explicitly specify the type of this.

function schedule(callback: (this: void) => void) {}
class C {
  data = 10;
  f(this: C) {}
}
const c = new C();
schedule(c.f); // Error

The problem: If we miss the type specification for either schedule or f, everything compiles without errors. Additionally, one of both places might not be in our control. For example, setTimeout does not specify the type of this for the callback. Therefore, in many situations, users are not able to get safe method invocations even if they explicitly specify the type of this for all their functions and methods.



:large_blue_diamond: Use unknown instead of void

A few weeks ago, I started a project with TypeScript. At one point, I was quite confused that the following assignment works even so, it contradicts [the description of void].

declare let x: (this: string) => any;
declare let y: (this: void) => any;
x = y;

The snipped compiles due to the modified assignability checks mentioned above. If we would use this: unknown instead of this: void, the special handling in the assignability checks would not be necessary, and it would not confuse new users like me. However, we can probably not revert the special handling since it would break a lot of existing code. However, if we want to change this in the long term, TypeScript could generate a warning whenever this is specified as void and recommend using unknown instead.



:large_blue_diamond: Revert part of Contextually typed this

I think an important objective is that the type for this is mostly consistent between the call-site and the method body. Whenever it becomes inconsistent, it enables incorrect usage. With this objective in mind, we should revert the contextual typing when the type is not specified.

const obj = {
  x: 0,
  f: function () {
    // Currently: Ok, `this` is contextually typed as `typeof obj`
    // Proposed: Error, because `this` is `void` (or `unknown`)
    this.x;
  },
  g() {
    // Continues to work sine it uses a method-style declaration
    this.x;
  }
};
// Currently: Ok, `obj.f` is typed as `(this: any) => void`
// Proposed (1): Ok, `obj.f` is typed as `(this: unknown) => void`
// Proposed (2): Error, `obj.f` is typed as `(this: void) => void`
obj.f.call(null);
// Currently: Ok, `obj.g` is typed as `(this: any) => void`
// Proposed: Error, `obj.g` is typed as `(this: typeof obj) => void`
obj.g.call(null);

interface I {
  x: number;
  f: (this: this) => void;
}
const obj2: I = {
  x: 0,
  f: function () {
    // Ok, `this` is contextually typed as `ThisType<I['f']>`
    // So, it does not change to the current behavior.
    this.x;
  },
};

I think the proposal in #6018 was kind of inconsistent in this regard.

// Copy from the proposal
interface I {
  f: (n: number) => string; // this: void
  g(n: number): string; // this: this
}
// Also from the proposal (but I removed a member)
let o = {
  data: 12,
  g: function() {   // this is inferred from the contextual type
    console.log(this.data); 
  }
}
// However, I think we should use the same rules as for interfaces and
// look at the style of the declaration.
let p = {
  data: 12,
  g() {   // `this` has type of `p`
    console.log(this.data); 
  },
  h: function() {   // `this` has type `void` (or `unknown`)
    console.log(this.data); // Error
  },
}



:large_blue_diamond: Avoiding performance hit

If I understood the conversations correctly, whenever we use this in the facade of a type, the type becomes generic, which then causes a loss in performance. As an approximation, we could avoid the usage of this in the facade but use the most specific concrete type we know.

class B {
  private x = 0;
  f() { // Has type `(this: B) => void` instead of `(this: this) => void`
    // Within the method body, we still consider `this` to have type `this`
  }
}
class C extends B {
  private y = 0;
  g() {} // Has type `(this: C) => void` instead of `(this: this) => void`
}
const c = new C();
// Compiles since due to the approximation, `this` has type `B` instead of `C`
c.f.call(new B());
// However, most cases like the following two are detected.
c.f.call(null); // Error
setTimeout(c.f, 0); // Error

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Roam-Cooper picture Roam-Cooper  ·  3Comments

CyrusNajmabadi picture CyrusNajmabadi  ·  3Comments

kyasbal-1994 picture kyasbal-1994  ·  3Comments

wmaurer picture wmaurer  ·  3Comments

MartynasZilinskas picture MartynasZilinskas  ·  3Comments