Repository navigation
Function this types #6018
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.
Anatoly Ressin (@Artazor) and Ron Buckton (@rbuckton) point out that this as argument may not be forward-compatible with future versions of Ecmascript. Ron Buckton (@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.
Activity
falsandtru commentedon Dec 9, 2015
Can you add a following syntax?
sandersn commentedon Dec 9, 2015
That's an extension of open question (2) -- can the contextual type of an object literal be used to contextually type a function member? Seems like the answer should be yes but I need to work through the details still.
falsandtru commentedon Dec 9, 2015
Should I create an another issue?
sandersn commentedon Dec 9, 2015
No, your example already works in Typescript,
thisjust has typeany. It's definitely part of this feature. I added your example to open question (2).falsandtru commentedon Dec 9, 2015
Thank you.
kitsonk commentedon Dec 10, 2015
👍 for seeing a well thought out proposal! Yay!
falsandtru commentedon Dec 12, 2015
I want an error for implicit
anytype ofthisusing a compiler option. Implicitanytype is dangerous.jeffreymorlan commentedon Dec 12, 2015
I think the best thing to do is default to
this: thisif using method syntax (foo(): string), default tothis: voidif using property syntax (foo: () => string)It's extremely common for interface members of function type to be implemented using class methods, which require that the correct
thisalways be passed:But it's also very common for interface members to be used as
this: voidstandalone functions. In particular, parameter interfaces often have callback functions which aren't necessarily called with the parameter object asthis:By distinguishing these two situations based on method syntax vs. property syntax, both of them are conveniently supported, and we preserve the intuitive fact that in
x.foo()has a type ofstring, andx.barhas a type of() => string(not(this: this) => stringor anything else).Same should go for object literals I guess:
a() { ... }has athistype of the enclosing literal,a: function() { ... }hasthis: void. A contextual type should override it in at least the latter case, though.Artazor commentedon Dec 14, 2015
Nathan Shively-Sanders (@sandersn) Nice work!
However my personal belief is that the constraint for the thisArg should look like
CallSignature:
TypeParametersopt ThisArgSpecopt
(ParameterListopt)TypeAnnotationoptFunctionType:
TypeParametersopt ThisArgSpecopt
(ParameterListopt)=>TypeThisArgSpec:
PrimaryType
::for example
or
and even more
What do you think?
Summoning Andrea Giammarchi (@WebReflection), Bergi (@bergus), Kevin Smith (@zenparsing), Ron Buckton (@rbuckton) while trying to align with ES7 bind operator :: and #3508
WebReflection commentedon Dec 14, 2015
I don't know if it's relevant for this case but regarding having a class with an
onClickevent and non accessiblethis, you might want to know or remember that since about ever you can define instead ahandleEventmethod which will automatically have the rightthis.Any
instanceof Handlerwill be invoked with the rightthisas long as the event will be set correctly.Apologies if this was unrelated but I've felt like it was a missing bit in the initial brainstorm about the
thiscontext issue.Best Regards
Artazor commentedon Dec 14, 2015
Andrea Giammarchi (@WebReflection) it is almost impossible to assign a correct type to the expression that involves free-form indirect member access. That is why
obj[arbitraryExpression]is chosen to be of typeany. Even with monstrous type system with dependent types we hardly could reason about these expressions statically.I've meant only a syntactical issue.
I prefer
over
What you do?
WebReflection commentedon Dec 14, 2015
if that's the problem you can use a switch statement but if it's about adding listeners, unless the operator goes out the way I've suggested, which is having
obj::method === obj::method, it's going to be even more impossible to ever remove any listener through the::form which is why I've said there are better patterns.About the
obj[method]form, the moment you invoke it's clearly a method that should throw if not.Otherwise I see
Reflect.apply(this[method], this, [event])as explicit enough intent for the method.The switch statement could be another solution that would make the
class Handlernot so portable or worth, so that we are back toswitch (event.type)within anhandleEventand invoke the related method, also avoiding to handle or invoke by accident that was not meant (kinda edge case though, tough to make it happen if not by accident as error).Apologies again if this was somehow a side argument/issue not strictly related.
Regards
rbuckton commentedon Dec 15, 2015
I am somewhat concerned about
thisin a parameter list as it could be forwards-incompatible should TC39 some day allowthisin a parameter list with actual semantics that differ from TypeScript's expectations.I'd generally prefer some other type-only mechanism for identifying the type of
thisthat is easily erased during emit. Anatoly Ressin (@Artazor)'s suggestion has some parity with some variants of the function-bind proposal, but it looks awkward, especially when you try to apply it to a function declaration:Although another approach might be the following, very C++ like example:
Another option would be to supply the type of
thisin the type parameter list:The proposed implementation does seem to align somewhat with C# extension methods:
18 remaining items
sandersn commentedon Apr 8, 2016
Matt McCutchen (@mattmccutchen) that's correct. Explicitly provided this is fully checked, but if you leave it off then it becomes any so that old code doesn't break. The now-removed strict-this flag just changed the default from any to void for functions and class-this for methods. As Mohamed Hegazy (@mhegazy) says, feel free to open an issue tracking the return of this flag. The remaining issues are slower compile time and backward compatibility, esp with DefinitelyTyped.
Igorbek commentedon Apr 8, 2016
Graeme Wicksted (@gwicksted) it's easy to type but it's definitely not easy to read. It's so easy to mess with formal parameter - it would need to know that would be a special case and at call site that argument wouldn't need to be passed.
I love the feature and was really looking forward to use it, but the syntax seems very unclear. I urge these who interested/participated in the discussion to reiterate your feeling about the syntax again.
saschanaz commentedon Apr 8, 2016
Did we have any great alternative syntax? I haven't seen one.
Igorbek commentedon Apr 8, 2016
4 options listed here: #6018 (comment) by Anatoly Ressin (@Artazor). I'm fully for IV and III.
gwicksted commentedon Apr 8, 2016
I'm especially for the ES7 bind compatible version simply because it is future proofing and essentially transpiling which is more the spirit of TypeScript.
Artazor commentedon Apr 9, 2016
I have already said that the bind-like version is my favourite, however I understand why the current syntax was chosen - it is much more simple to parse/implement, it allows ThisArg to be generic, and it ressembles C#, so I think Igor Oleinikov (@Igorbek) we have little chances to make any influence on it. It is the battle: simplicity versus purity. Simplicity wins
saschanaz commentedon Apr 9, 2016
Also, unfortunately the ES7 bind proposal is still on stage 0.
Igorbek commentedon Apr 9, 2016
I have a question inspired by removed
strictThisChecksoption explanation. Say we have a code:Theoretically,
const i: I = ashouldn't work, becausea.fis of type(this: A) => voidandi.fis of type(this: I) => void, and these types must be incompatible, however they are compatible at the moment.Actually we can replace
thisparameter to any formal parameter of typethisand get same behavior on current bits.sandersn commentedon Apr 11, 2016
Igor Oleinikov (@Igorbek), I don't get an error with
b.f = a.funless I add a field to B, likeprivate b = 1. At any rate, you are running into parameter bivariance, which is an unfortunate hole in the type system, but a by-design one to let us avoid variance annotations like Java. You can see this with any parameter, not just ones of typethis:As for syntax, try writing
Function.bindwith the::syntax:I am not sure if this would parse, actually. Even for humans, it forms a garden path construction since you can't tell whether the first
::is nested or not. Maybe it would help if parentheses were required for the nested this-type.Igorbek commentedon Apr 12, 2016
Required parenthess for the nested this-type would really help:
For me, even this syntax looks much better still.
Igorbek commentedon Apr 12, 2016
Nathan Shively-Sanders (@sandersn) thank you for clarification, that it's caused by parameter bivariance. But if we hadn't it, any usage of
thistype in class/interface hierarchy would make a derived class to be nonassignable to base type.farfromrefug commentedon Apr 13, 2016
Would it make sense for module to return
thisin one of its methods?I am trying to build a ts doc for Titanium Mobile.
In titanium you can create object like this
Now in this line there 3 types ,
Titanium,Titanium.UI,Titanium.UI.ViewThe true defintion in the SDK is this:
For now the way chosen to declare this in typescript is this.
This works really well except for the method
addEventListenerTitaniumandUIare actuallyProxyobject so they haveaddEventListenermethod.As we can't say that a module
implementswe have toexportthe method for each module.But the problem is that the return type
thiswon't work for modules.I understand that
moduleare actually kind of avar, is there any way to export a function that returns the module itself?Or maybe there is another way to declare such a structure?
sandersn commentedon Apr 13, 2016
You could try merging an interface and a module. But I couldn't figure out how to nest Proxy inside Titanium using this method. Here's what I got:
A few things to note:
thisisn't a this-function type here, but a this-class type.farfromrefug commentedon Apr 14, 2016
Nathan Shively-Sanders (@sandersn) Thanks a lot will try that! And sorry will put that discussion in stackoverflow.
Thanks