-
Notifications
You must be signed in to change notification settings - Fork 13.1k
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:
thisis the class'thistype for methods.thisis not bound for lambdas.thisis of typevoidfor 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 objectCall-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
thisfor methods. - not present for lambdas.
- of type
voidfor 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
thisbe 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
thisof interface methods use?
@jeffreymorlan suggests usingthisfor method-style declarations andvoidfor 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
thisparameters 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.