Description
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.
- Assignability checking for callbacks -- methods must match methods and lambdas must match lambdas.
- Objects built from standalone functions.
- 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:
- C#-like:
function filter<T> (this: Iterable<T>, callback: (value: T) => boolean): Iterable<T>
- C++-like:
function Iterable<T>::filter<T>(callback: (value: T) => boolean): Iterable<T>
- 'ES7-bind'-like:
function filter<T> Iterable<T>::(callback: (value: T) => boolean): Iterable<T>
- this-type-parameter:
function filter<T, this extends Iterable<T>>(callback: (value: T) => boolean): Iterable<T>
Semantics
The semantics fall into 3 areas
- Function body checking.
- Assignability checking.
- 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 typevoid
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
-
How should loose-this work?
- No annotations allowed.
- No checking at call sites or assignments.
- Default to any or missing at call sites or assignments.
-
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 } }
-
What should
this
of interface methods use?
@jeffreymorlan suggests usingthis
for method-style declarations andvoid
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. -
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:
- Lambdas should work.
- Interfaces should work.
- Strict mode should actually be strict.
- Separate representation for this-parameters.
- Tests to ensure that type parameters and type guards work.
- Loose mode should work.