Skip to content

Function this types #6018

Closed
Closed
@sandersn

Description

@sandersn

This is a proposal for the other half of this-types outlined in #3694. The first half -- class 'this' types -- was implemented in #4910.

Motivation

This-types for functions allows Typescript authors to specify the type of this that is bound within the function body. Standalone functions are an important part of Javascript programming, so Typescript's ability to check the type of this will capture patterns that it does not today. This feature enables three main scenarios.

  1. Assignability checking for callbacks -- methods must match methods and lambdas must match lambdas.
  2. Objects built from standalone functions.
  3. Specific sub-type requirements on the caller of a method that can't be captured in an inheritance hierarchy.

Typescript currently sets the type of this to any except in when checking method bodies, where it is the class' this type. To be backward compatible, almost all of this feature will be hidden behind a --strictThis flag at first. This is because some working Javascript patterns will not be legal until they are annotated.

Examples
These examples assume that --strictThis is enabled. I'll add examples without --strictThis later.

Prevent incorrect assignment between callback functions and methods

declare class Callbacks {
  onClick(e: Event): void; // this defaults to Callback's this type
}
function handleClick(e: Event) {
  // Callback's this is not accessible here
}
function handleClick2(this: Callbacks, e: Event) {
  // Callback's this *is* accessible here
}
declare class Methods {
 clickMethod(e: Event): void; // this defaults to Method's this type
}
let c = new Callbacks();
let m = new Methods();
// assign methods
c.onClick = handleClick; // ok 
c.onClick = handleClick2; // ok 
c.onClick = m.clickMethod; // ERROR: 'Mine' is not assignable to 'Callbacks'
c.onClick = e => console.log("this is not defined inside a lambda"); // ok 

Build new objects from existing functions and methods

let f = function(this: {data: number}) {
  console.log(this.data2) 
}
let o = {
  data: 12
  f: f
  g: function() {   // this is inferred from the contextual type
    console.log(this.data); 
  }
}

Require a suitable 'this' for substituting the current one

For example, Function.call allows the caller to specify a new this. This results in an interesting type:

interface Function {
  call<This,Return>(this: (this:This, ...args:any[]) => Return, 
                                  thisArg: This,
                                  ...args:any[]):
    Return;
}

Syntax

this is an optional first argument to all functions and methods except for lambdas. This syntax is a good representation of Javascript's actual semantics, where this is available inside all function bodies and is checked the same way as any other parameter once its type is known.

Examples of the syntax:

function f(this: {n: number}, m: number) {
    return this.n + m;
}
class C {
  n: number
  m1(this:this, m: number) {
    return this.n + m
  }
  m2(this: {n: number}, m: number) {
    return this.n + m
  }
}

Note that, although it is syntactically an argument, this is treated specially during checking and erased at emit.

@Artazor and @rbuckton point out that this as argument may not be forward-compatible with future versions of Ecmascript. @rbuckton listed the alternatives proposed so far:

  1. C#-like: function filter<T> (this: Iterable<T>, callback: (value: T) => boolean): Iterable<T>
  2. C++-like: function Iterable<T>::filter<T>(callback: (value: T) => boolean): Iterable<T>
  3. 'ES7-bind'-like: function filter<T> Iterable<T>::(callback: (value: T) => boolean): Iterable<T>
  4. this-type-parameter: function filter<T, this extends Iterable<T>>(callback: (value: T) => boolean): Iterable<T>

Semantics

The semantics fall into 3 areas

  1. Function body checking.
  2. Assignability checking.
  3. Call-site checking.

Function Body Checking

Function bodies are checked as if this were a normal parameter. References to this in the body are required to satisfy the type provided for this. If this is not provided:

  • this is the class' this type for methods.
  • this is not bound for lambdas.
  • this is of type void for functions (for backward compatibility).

For unannotated methods and lambdas, the behaviour does not change from current Typescript. For unannotated functions, the void type has no members, so uses of this are essentially disallowed. This will have to change for "loose this" mode.

Examples:

class C {
  n: number;
  m1() { console.log(this.n); }// ok: this is C's this
  m2(this: void) { console.log(this.n); } // ERROR: void has no member 'n'
}
function f(this: {n: number}) { console.log(this.n); }
function g() { console.log(this.n); } // ERROR
f.call(new C()); // use C as `this`
let lambda = () => this.notThere; // ERROR: `this` is the global object

Call-Site Checking

Call sites check a this argument against the function or method's this parameter. To determine the this argument:
• If the call is of the form o.f(), the type of this is the type of o.
• If the call is of the form f(), the type of this is void.

For example:

declare function f(this:void, n: number);
f(12); // ok: no object, so this: void, which matches f's `this`.

The this parameter's type can be given, as in the previous example. If it is not, then, similarly to body checking, it is:

  • the class' type this for methods.
  • not present for lambdas.
  • of type void for functions.

It is important that methods cannot be called without an object as if they were standalone functions. Given the types above, normal assignability rules will ensure this. On the other hand, functions actually can be called as if they were methods -- they just happen not to refer to this. To support this, we add an exception to normal assignability rules that applies when calling a function as if it were a method.

Specifically, when the callee's this type -- the this parameter -- is of type void, a non-void this type will satisfy it. Let's look at examples of the two cases:

declare class C { m(); }
let c = new C();
let f = c.m; // ok, f: (this:C) => void
f(); // ERROR, standalone call -- 'this:void' is not assignable to 'this:C'

declare function g(this:void, n: number);
let o = { g };
o.g(12); // ok: even though `o` is not void, you can still call `g`.

Here is an example using lambdas to build up an object literal:

let o = { n: 12, f: m => m + 1 };
o.f(12); // ok: `this:o`, but the lambda ignores it.

Assignability Checking

The rules for assignability are similar to those for call checking. The only complication is that default types for this have to be determined for both the source and the target. Like call checking, they conspire to make both functions and lambdas assignable to methods, but methods not assignable to functions.

Open questions

  1. How should loose-this work?

    1. No annotations allowed.
    2. No checking at call sites or assignments.
    3. Default to any or missing at call sites or assignments.
  2. Should function literal's this be contextually typed?
    It would make callback-like methods require no additional type annotations to be fully checked:

    c.callback = function(arg) { return arg + this.property; }

    Notably, allowing the contextual type of an object literal to contextually type a function member would allow checked ad-hoc construction from motivating scenario (2):

    let o = {
      n: 12
      f: function() { return this.n }
    }
  3. What should this of interface methods use?
    @jeffreymorlan suggests using this for method-style declarations and void for function-style declarations. This aligns nicely with the increasing rift in method-style versus function-style declarations in ES2015 and ES2016.

    interface I {
      f: (n: number) => string; // this: void
      g(n: number): string; // this: this
    }

    This will still add a lot of this parameters to interfaces, but they will mostly be desired ones.

  4. What if an interface is merged with a class? Does this change anything?

Implementation Progress

A prototype is at sandersn/TypeScript/check-this-function-types. It checks function and methods bodies, call sites and assignability but does not erase the 'this' parameter during emit. It also stuffs 'this' into the parameter list whenever it's convenient. It is somewhere between strict-this and loose-this in this proposal.

Here's what's left to do:

  1. Lambdas should work.
  2. Interfaces should work.
  3. Strict mode should actually be strict.
  4. Separate representation for this-parameters.
  5. Tests to ensure that type parameters and type guards work.
  6. Loose mode should work.

Metadata

Metadata

Assignees

Labels

FixedA PR has been merged for this issueSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions