Repository navigation
Function this types #6018
Description
Activity
- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptIn DiscussionNot yet reached consensusNot yet reached consensus
on Dec 9, 2015 Can you add a following syntax?
let o = { data: 12, ... h() { // this is inferred from the contextual type console.log(this.data); } }
sandersn commented
on Dec 9, 2015 MemberAuthorMore actionsThat'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.
Should I create an another issue?
sandersn commented
on Dec 9, 2015 MemberAuthorMore actionsNo, your example already works in Typescript,
thisjust has typeany. It's definitely part of this feature. I added your example to open question (2).Thank you.
👍 for seeing a well thought out proposal! Yay!
I want an error for implicit
anytype ofthisusing a compiler option. Implicitanytype is dangerous.How should interfaces work?
Should the default bethis? Missing?any?void?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:interface Displayable { display(): string; } class Person implements Displayable { ... display() { return this.firstName + " " + this.lastName; } }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:interface TableParams { title: string; filter: (p: Person) => boolean; ... } function createTable(params: TableParams) { Person.getAll().filter(params.filter).forEach(p => { // params.filter gets called with this === undefined, not with this === params }); }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
var x: { foo(): string; bar: () => string; }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.Nathan Shively-Sanders (@sandersn) Nice work!
However my personal belief is that the constraint for the thisArg should look like
CallSignature:
TypeParametersopt ThisArgSpecopt(ParameterListopt)TypeAnnotationopt
FunctionType:
TypeParametersopt ThisArgSpecopt(ParameterListopt)=>Type
ThisArgSpec:
PrimaryType::for example
type SomeFn = MyClass::(a: number) => string var mc: MyClass = ...; var fn: SomeFn = ...; console.log(mc::fn(123)); // ok console.log(20::fn(123)); // error: type number is not compatible with type MyClass
or
type GetOne = <T>Array<T>::() => T; var pop = <GetOne>Array.prototype.pop; var shift = <GetOne>Array.prototype.shift; var x = [1,2,3]::pop() // x is number var y = ["a","b"]::shift() // y is string var z = {x: 7}::pop() // error
and even more
class A { ... } class B extends A { ... } class C { ... } type ASelfMethod = A::(x: number) => this; var a: A, b: B, c: C, fn: ASelfMethod; ... var a2 = a::fn(123) // a2: A var b2 = b::fn(123) // b2: B var c2 = c::fn(123) // error
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 commented
on Dec 14, 2015 More actionsI 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.class Handler { handleEvent(e) { let type = e.type; this['on' + type[0].toUpperCase() + type.slice(1)](e); } }
Any
instanceof Handlerwill be invoked with the rightthisas long as the event will be set correctly.class Clicker extends Handler { constructor(el) { this.el = el; this.counter = 0; el.addEventListener('click', this); } onClick(e) { e.preventDefault(); this.counter++; console.log(this.counter); console.log(this instanceof Clicker); // always true } }
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
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
type MyMethod = MyClass::(x: number) => string
over
type MyMethod = (this: MyClass, x: number) => string
What you do?
WebReflection commented
on Dec 14, 2015 More actionsif 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
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:function filter<T> Iterable<T>::(callback: (value: T) => boolean): Iterable<T> { ... }
Although another approach might be the following, very C++ like example:
function Iterable<T>::filter<T>(callback: (value: T) => boolean): Iterable<T> { ... }
Another option would be to supply the type of
thisin the type parameter list:function filter<T, this extends Iterable<T>>(callback: (value: T) => boolean): Iterable<T> { ... }
The proposed implementation does seem to align somewhat with C# extension methods:
static IEnumerable<T> Filter(this IEnumerable<T> source, Func<T, bool> callback) { ... }
18 remaining items
sandersn commented
on Apr 8, 2016 MemberAuthorMore actionsMatt 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.
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 commented
on Apr 8, 2016 ContributorMore actionsDid we have any great alternative syntax? I haven't seen one.
4 options listed here: #6018 (comment) by Anatoly Ressin (@Artazor). I'm fully for IV and III.
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.
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 commented
on Apr 9, 2016 ContributorMore actionsAlso, unfortunately the ES7 bind proposal is still on stage 0.
I have a question inspired by removed
strictThisChecksoption explanation. Say we have a code:interface I { f(this: this): void; //f this::(): void; // wouldn't it be nicer and clearer? :) } class A implements I { private a = 1; f(this: this) { //f this::() { this.a.toFixed(); } } class B implements I { f(this: this) { } //f this::() { } } const a = new A(); const b = new B(); b.f = a.f; // error const i: I = a; // allowed? b.f = i.f; // ok? b.f(); // boom!
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 replacethisparameter to any formal parameter of typethisand get same behavior on current bits.sandersn commented
on Apr 11, 2016 MemberAuthorMore actionsIgor 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:interface II { i: number; } class AA implements II { i = 12; a = 1; } class BB implements II { i = 13; b = 1; } let z: (arg: II) => void = function(arg: AA) { }; // legal :( z = function(arg: BB) { }; // also legal :( z(new AA()); // legal, error at runtime
As for syntax, try writing
Function.bindwith the::syntax:interface Function { // ... bind<T,U>(this: (this: T, ...args:any[]) => U, thisArg: T, ...args: any[]): (...args: any[]): U; // vs bind<T,U> T::(...args:any[]) => U::(thisArg: T, ...args: any[]): (...args: any[]) => U;
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.Required parenthess for the nested this-type would really help:
interface Function { bind<T,U> (T::(...args:any[]) => U)::(thisArg: T, ...args: any[]): void::(...args: any[]) => U; } // or type Func<T, R> = T::(...args[]) => R; interface Function { bind<T,U> Func<T, U>::(thisArg: T, ...args: any[]): Func<void, U>; }
For me, even this syntax looks much better still.
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.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 thislet a: Titanium.UI.View = Titanium.UI.createView();
Now in this line there 3 types ,
Titanium,Titanium.UI,Titanium.UI.View
The true defintion in the SDK is this:class Proxy class Module extends Proxy global var Titanium: Module Titanium.UI: Module class Titanium.UI.View extends Proxy function Titanium.UI.createView() : Titanium.UI.View
For now the way chosen to declare this in typescript is this.
export module Titanium { export interface Proxy { addEventListener(name:string, callback: Function) : this } export module UI { export interface View extends Proxy { } export function createView(): Titanium.UI.View } }
This works really well except for the method
addEventListener
TitaniumandUIare actuallyProxyobject so they haveaddEventListenermethod.
As we can't say that a moduleimplementswe have toexportthe method for each module.
But the problem is that the return typethiswon'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 commented
on Apr 13, 2016 MemberAuthorMore actionsYou 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:
declare interface Pxy { addEventListener(name: string, callback: Function): this; } declare namespace Titanium { export namespace UI { export interface View extends Pxy { } export function createView(): Titanium.UI.View } } declare interface Titanium extends Pxy { something(foo: number): this; } /// in main.ts, /// <reference path="titanium.d.ts" /> let t: Titanium = undefined; // not sure how to get an instance of Titanium. t.something(12); let view = Titanium.UI.createView();
A few things to note:
thisisn't a this-function type here, but a this-class type.- Modules are now called namespaces. Standard Ecmascript modules are now called modules.
- Did you intend to use ES modules by exporting the top-level module? Or did you intend to just create the module in the global space?
- Since this a code design question, it's better suited to Stack Overflow than to a github design discussion.
Nathan Shively-Sanders (@sandersn) Thanks a lot will try that! And sorry will put that discussion in stackoverflow.
Thanks- locked and limited conversation to collaborators
on Jun 19, 2018
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
thisthat is bound within the function body. Standalone functions are an important part of Javascript programming, so Typescript's ability to check the type ofthiswill capture patterns that it does not today. This feature enables three main scenarios.Typescript currently sets the type of
thistoanyexcept in when checking method bodies, where it is the class'thistype. To be backward compatible, almost all of this feature will be hidden behind a--strictThisflag at first. This is because some working Javascript patterns will not be legal until they are annotated.Examples
These examples assume that
--strictThisis enabled. I'll add examples without--strictThislater.Prevent incorrect assignment between callback functions and methods
Build new objects from existing functions and methods
Require a suitable 'this' for substituting the current one
For example,
Function.callallows the caller to specify a newthis. This results in an interesting type:Syntax
thisis an optional first argument to all functions and methods except for lambdas. This syntax is a good representation of Javascript's actual semantics, wherethisis available inside all function bodies and is checked the same way as any other parameter once its type is known.Examples of the syntax:
Note that, although it is syntactically an argument,
thisis treated specially during checking and erased at emit.Anatoly Ressin (@Artazor) and Ron Buckton (@rbuckton) point out that
thisas argument may not be forward-compatible with future versions of Ecmascript. Ron Buckton (@rbuckton) listed the alternatives proposed so far:function filter<T> (this: Iterable<T>, callback: (value: T) => boolean): Iterable<T>function Iterable<T>::filter<T>(callback: (value: T) => boolean): Iterable<T>function filter<T> Iterable<T>::(callback: (value: T) => boolean): Iterable<T>function filter<T, this extends Iterable<T>>(callback: (value: T) => boolean): Iterable<T>Semantics
The semantics fall into 3 areas
Function Body Checking
Function bodies are checked as if
thiswere a normal parameter. References tothisin the body are required to satisfy the type provided forthis. Ifthisis 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
thisare essentially disallowed. This will have to change for "loose this" mode.Examples:
Call-Site Checking
Call sites check a
thisargument against the function or method'sthisparameter. To determine thethisargument:• If the call is of the form
o.f(), the type ofthisis the type ofo.• If the call is of the form
f(), the type ofthisis void.For example:
The
thisparameter's type can be given, as in the previous example. If it is not, then, similarly to body checking, it is:thisfor methods.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
thistype -- thethisparameter -- is of typevoid, a non-voidthistype will satisfy it. Let's look at examples of the two cases:Here is an example using lambdas to build up an object literal:
Assignability Checking
The rules for assignability are similar to those for call checking. The only complication is that default types for
thishave 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?
Should function literal's
thisbe contextually typed?It would make callback-like methods require no additional type annotations to be fully checked:
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):
What should
thisof interface methods use?jeffreymorlan suggests using
thisfor 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.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: