Repository navigation
Proposal to improve static analysis for the thisArg of a function #1985
Description
Activity
-
When you say that
method.bind({})();(where method has thisArg of type SomeType) is an error, is it an error at the call to bind or the call to the bound function? I guess it's the former. -
If type Foo is assignable to type Bar, is
function<this: Foo>()assignable tofunction<this: Bar>()? Isfunction<this: Bar>()assignable tofunction<this: Foo>()? I guess the first is no and the second is yes. -
Is
function<this: any>()assignable tofunction<this: Foo>()? Isfunction<this: Foo>()assignable tofunction<this: any>()? I guess both are yes (this follows from the previous point, since all types are assignable toanyandanyis assignable to all types). -
Is any function without an explicit thisArg considered to have a thisArg of type
any? (I think this would maintain current behavior.) Do fat arrow functions fall in this category? What about class methods, since you say they don't have an implicit thisArg that's the type of the class? -
Is it allowed for the thisArg to be
void? This would cover the case where a function takes a callback and calls it without providing a particularthisvalue. In unstrict modethisinside the callback would be the global object, and in strict mode it would be undefined, so it would be nice to be able to say a callback parameter has<this: void>so thatthiscannot be accidentally used inside the callback argument. -
Does the result of bind automatically get a thisArg type? Say:
var foo: Foo; var bar: () => void; var baz = bar.bind(foo);
Is baz of type
function(): voidorfunction<this: Foo>(): void? If the latter, this might introduce the same breaking changes that you're trying to avoid by not having an implicit thisArg on class methods, so I guess it's the former. -
You have an example of an interface method with a thisArg type that's not the same as the interface. This can be easily implemented by object literals, but how would a class implement such an interface? That is, are class methods also allowed to have a thisArg type that's not the same as the class type? Or is it simply impossible for a class type to implement such an interface. If the former, then such a method would have to be disallowed to be called on an instance of the class:
interface Foo { foo: number; } class Bar { bar: string; someMethod<this: Foo>() { return this.foo; } } var barInstance = new Bar(); barInstance.someMethod(); // Error? class Baz extends Bar { foo = 5; } var bazInstance = new Baz(); bazInstance.someMethod(); // Valid?
-
Is this allowed?
class Foo { foo: number; someMethod() { return this.foo; } } interface Bar { bar: string; } var foo = new Foo(); foo.someMethod = function<this: Bar>() { return this.bar.length; } // Error? foo.someMethod(); // Direct call to redefined someMethod(). Error? (function (x: Foo) { x.someMethod(); })(foo); // Indirect call to redefined someMethod(). Error?
Since someMethod doesn't have an explicit thisArg nor an implicit thisArg per your proposal, it would seem it's allowed.
-
Arnav Singh (@Arnavion) Thanks for identifying some of the issues with my proposal.
Here are my solutions:
-
While it would be nice to make
method.bind({})()error at call time (for consistency with other possible errors introduced by this proposal, I feel it could be confusing and hard to find the source of this error if we did do that. For that reason, it should fail when calling bind. -
When specifying a desired type for the
thisArg, passedthisArgs must be either of that type or a subtype of that.Example:
class Foo {} class Bar {} class FooBar extends Foo {} declare function someFunction(someCallback : <this : Foo>() => void); someFunction.call(new Foo()); // okay someFunction.call(new FooBar()); // okay someFunction.call(new Bar()); // error
-
Yes, for the reasons you said.
-
Yes. Fat arrows do also fall in to this category. Class methods are a bit of both. From outside, they have their
thisArgspecified asanyinherently (it's actually a syntax error to try to set it explicitly as class methods are outside of the scope of this proposal). However, inside the body of a class method, thethisArgis assumed to be the type of the class in which the method is defined. This is to maintain current functionality.class Foo { someMethod() { // this is assumed to be of type Foo } // syntax error as doing this is outside the code of this proposal // to simplify a lot of things // Can look at adding support for this once the proposal is accepted someOtherMethod<this : number[]>() { } } var foo = new Foo(); var method = foo.someMethod(); foo.someMethod(); // Allowed method(); // Allowed method.call({}); // Allowed method.apply({}); // Allowed method.bind({})(); // Allowed
Note: If I haven't explained this well enough, let me know. I'll try again.
-
Yes. TypeScript should assume that
this === undefinedinside the body offunction<this : void>s. -
The type of
thisArgcan only be set at definition time. Using bind has no effect on this as long as you're binding to a compatible type. Trying to bind to an incompatible type is an error as explained in the proposal. -
This is not actually an interface method, it's a interface defining a function. Interface methods are not within the scope of this proposal as class methods are not within this proposal.
-
Your first call with fail. The second won't, as it expects
xto be of typeFoo, sox.someMethodhas an inherent type offunction<this : any>(though code inside its body will assume that it is of typeFoo, as explained in point 4.
I hope this answers some of your questions.
-
I would just like to say, for me at least, adding new syntax that further conflicts with React's JSX would be a bad idea. If the proposed syntax conflicts with the implementation of jsx-typescript over at https://github.com/fdecampredon/jsx-typescript/ then I think the syntax should change.
(Look at react/react#759 for more info on TypeScript/JSX compatibly).
If the proposed syntax conflicts with JSX (which it seems like it would, but this should be verified), here are some other possibilities:
- declare function someFunction(this : SomeType, someArg : SomeArgType): any;
- declare function someFunction((this : SomeType), someArg : SomeArgType): any;
- declare function someFunction(this : SomeType)(someArg : SomeArgType): any;
- declare function someFunction(SomeType)(someArg : SomeArgType): any;
- declare function someFunction : SomeType (someArg : SomeArgType): any;
The biggest advantages of the proposed syntax are:
- It won't conflict with any other syntax rules, since right now angle brackets cannot be in a function signature at all
- It's clear to readers that the type is declared for "this" without prior knowledge of the syntax (unlike 4 and 5 above)
For those reasons, I think 3 is the best replacement for the proposed syntax.
I would also like to mention that I think this is a very important feature for the future of TypeScript, as many JS libraries rely on this-binding. The specific library where I found this problematic was @meteor-typescript.
Jason Meisel (@jasonmeisel) Your first advantage is incorrect, as angle brackets can be in a function signature. Angle brackets are used for TypeScript generics.
Example: (view in TypeScript playground)
class AbstractBuzz { foo: number; } class BuzzImpl extends AbstractBuzz { bar: string; } class Wrapper<T> { constructor(public wrapped: T) {} } function wrapIt<T extends AbstractBuzz>(instanceToWrap: T): Wrapper<T> { return new Wrapper(instanceToWrap); } var wrappedBuzz = wrapIt(new BuzzImpl()); console.log(wrappedBuzz.wrapped.bar); // Compiles correctly thanks to generics.EDIT: A much better example of generic function signatures are the underscore / LoDash type definitions: https://github.com/borisyankov/DefinitelyTyped/blob/master/lodash/lodash.d.ts
For more examples of generics, look at the declarations for Q.js
// Some trivial example. Would never do this var nums = Q.Promise<number[]>(resolve => { resolve([1, 2, 3]); }); nums.then(nums => { nums // typescript knows this is a number[] });
Jason Meisel (@jasonmeisel) The guys over at React feel this wouldn't conflict with JSX anymore than TypeScript already does, and wouldn't break their current implementation. I propose we stick with the originally suggested syntax of
function<this : Type>for this reason.Andrew Bradley (@cspotcode) Nate Higgins (@nathggns) My mistake, I forgot about function generics :) If the proposed syntax doesn't break any existing systems, I am all for it!
1.For that reason, it should fail when calling bind.
I do agree it should fail when calling bind. I just wanted to confirm that was your intent.
2.When specifying a desired type for the
thisArg, passedthisArgs must be either of that type or a subtype of that.That is not what I asked. I asked about the assignability of a function type with a particular thisArg to another function type with a different thisArg based on the assignability of the type of the thisArgs.
7.This is not actually an interface method, it's a interface defining a function. Interface methods are not within the scope of this proposal as class methods are not within this proposal.
Sorry, I misread your example. That said, by allowing them on the function operator you're also allowing them on interface and class methods. Consider:
interface FooMethod { <this: Foo>(x: string): number; } interface Bar { func: FooMethod; // Equivalent to func<this: Foo>(x: string): number; }
Do you intend that func's thisArg type should revert to
anyin this case?8.Your first call with fail. The second won't, as it expects
xto be of typeFoo, sox.someMethodhas an inherent type offunction<this : any>(though code inside its body will assume that it is of typeFoo, as explained in point 4.Can you clarify what you mean by "first call" ? There are three lines in my example - the assignment, the direct call on the instance, and the indirect call through the class type. Which of these would compile with your proposal and which wouldn't?
If by "first call" you mean the
foo.someMethod()line, then this is surprising. I don't expect any difference between the two method calls (the second and third lines), as a difference there would require foo to stop being of type Foo, which TS does not support. So either all three lines should compile, or the assignment (the first line) should not compile and the other two lines become irrelevant.That is not what I asked. I asked about the assignability of a function type with a particular thisArg to another function type with a different thisArg based on the assignability of the type of the thisArgs.
Sorry do you mean the following?
class A {} class B extends A {} var x = function<this : number>(){}; var y = function<this : string>(){}; var z = function<this : A>(){}; var zz = function<this : B>(){}; x = y; // Error y = x; // Error zz = z; // Error z = zz; // Okay
Do you intend that func's thisArg type should revert to any in this case?
I think trying to set a property of an interface to a function interface that has a specified
thisArgshould be an error at this point, in order to avoid undefined behaviour. Eventually, the intention is to support explicitly setting thethisArgfor class and interface methods but that is outside of the scope of this initial proposal, so I would make it an error for now.Can you clarify what you mean by "first call" ? There are three lines in my example - the assignment, the direct call on the instance, and the indirect call through the class type. Which of these would compile with your proposal and which wouldn't?
Sorry I was being short-sited here. Yes, the assignment should error in order to stop changing Foo.
class Foo { someMethod() { } } var foo = new Foo(); foo.someMethod = function<this : number>() {}; // Error foo.someMethod(); // Wouldn't even reach this.
Thanks, that answers my questions. You could put these points into your proposal post.
Arnav Singh (@Arnavion) I have done that now.
Can I ask, do you support this proposal? While I know it would help me massively, and possibly the guys over at DefinitelyTyped (@DefinitelyTyped), I'm not sure how much other people need this feature.
I have been subscribed to #229 for a long time, so yes I do support it :)
this-typing is very helpful for callbacks, of course, but my own use for this feature would be to annotate interface methods with
<this: void>(see #1545 (comment)) and to see how implementing such an interface would look like. I know your proposal specifically excludes this-typing on interface methods, but it's a starting step.I'm not sure how much other people need this feature.
For what it's worth, I'm only an average TypeScript developer and constantly litter my code with
var self = <MyType>this;just to get "this" type support. I think it's really greatly needed, especially with any prototypal inheritance being done. In my opinion it's a blindingly obvious hole in the language.Thank you. Would've been a bit rubbish to have written the proposal and for no one to really want it.
Sent from my iPhone
On 10 Feb 2015, at 03:19, Aaron Holmes [email protected] wrote:
I'm not sure how much other people need this feature.
For what it's worth, I'm only an average TypeScript developer and constantly litter my code with var self = this; just to get "this" type support. I think it's really greatly needed, especially with any prototypal inheritance being done. In my opinion it's a blindingly obvious hole in the language.
—
Reply to this email directly or view it on GitHub.28 remaining items
"TypeScript intends to add a type system onto vanilla ECMAScript, which means not covering up JS semantics"
Why does it mean that? TypeScript is, to me, just a "better" language based on JavaScript, that happens to compile to JavaScript. If it continue to propagate things that make JavaScript difficult to code well with then it's failed in being an improvement.
Understood about the complete spec - my main concern is still some sort of "smart" detection that warns when it appears you're probably misusing "this", i.e. when the method/field you're referring to after "this." is one in the current class. Even with full typing for 'this' such a mistake is possible.I'm just saying that, realistically, proposals that stray too far from the TS developers' goals are likely to be rejected. They want to align with ECMAScript 6, adding a type system and tooling on top of Javascript, allowing easy interop with existing Javascript libraries. They intentionally avoid overly-complex code transformations. It's fairly easy to migrate existing codebases to TypeScript because they've focused on a type system and tooling, not on changing language semantics. Even the current arrow functions and classes compile into ES6 arrow functions and classes.
If the goal is merely a far better language, one might argue you should write C# and compile that into JS.
For syntax I'd propose
declare class MyClass { someMethod(x: integer); } var x = function[MyClass](x: integer) { // this: MyClass this.someMethod(x); }
And for generics:
declare class OtherClass<T> { someMethod(arg: T); } var y = function<T>[OtherClass<T>](x: T) { // this: OtherClass<T> this.someMethod(x); }
About type system (roughly):
thistype should be strictly co-variated since we can not assign tothis. And that is why in TS it can't be an additional parameter.Sorry for arriving late at this, as the biggest "item" is the ability to do essentially late binding of typing to allow chaining. I have seen a fair few machinations, but maybe we are missing something that would not impede compatibility with JavaScript/EcmaScript and would be essentially very much TypeScript without challenging generic usages. Admittedly, I am not at all familiar with the sort of hoops that this "late" typing would have on the compiler, but what about the following syntax:
interface A { foo(): typeof this; } class B implements A { foo(): typeof this { return this; } } class C extends B { bar(a: typeof this): typeof this { return a.foo(); } } let c = new C(); c.foo().bar(c).foo(); /* should be totally valid */
So essentially, instead of a version of generics, or something else, whenever you need to type something of the current class (or validate that something is implemented an interface properly) you would use
typeof this.While, I haven't worked through all the emitting of the above, I think it could easily be determined and that the "reserved" type of
typeof thiswould always be determined within the context of block it was existing in, meaning it can be scoped properly.pmccloghrylaing commented
on Jun 10, 2015 More actionsKitson Kelly (@kitsonk) I think that suggestion belongs in #285.
My vote is for
thisas the first argument, without a special separator. It seems to more accurately represent what is happening without adding a special syntax. I think by includingthis : anyimplicitly for functions that don't specifythismeans assignability is already taken care of asthisis treated as another argument. It should also make things easier to implement. Unfortunately this does make assignability looser than it could be (see: Function Argument Bivariance).Nate Higgins (@nathggns): your proposal has assignability the wrong way around for
zandzz:class A {} class B extends A {} var z = function<this : A>(){}; var zz = function<this : B>(){}; z.call(new A()); // Okay z.call(new B()); // Okay zz = z // Okay zz.call(new A()); // Error zz.call(new B()); // Okay z = zz; // Error
jussi-kalliokoski commented
on Jun 16, 2015 More actionsFWIW, I (unaware of this issue) opened a similar one on facebook/flow#452, in my last comment proposing the same syntax that was proposed here (e.g.
function add (this : number, b: number) { return this + b; }).I think that would be the most intuitive syntax for this, and I use it in trine for annotating function types (I use
_thisbecause babel currently considersthisas an illegal parameter name). This would nicely also leave room for something like a namedthissyntax which would make trine's paradigm more readable (and also give full control over the scoping and destructuring ofthis):function add (&a : number, b : number) { return a + b; } function vec2add (&[x1, y1], [x2, y2]) { return [ x1 + x2, y1 + y2 ]; } // bind syntax: [1,2]::add([5,3]) // [6, 5]
With respect to the new ES7 "::" bind operator I'd propose
var abc = function MyClass::(x: integer) { // this: MyClass this.someMethod(x); } function MyClass::abc(x: integer) { // this: MyClass this.someMethod(x); } var abc = function<T> OtherClass<T>::(x: T) { // this: OtherClass<T> this.someMethod(x); } function<T> OtherClass<T>::abc(x: T) { // this: OtherClass<T> this.someMethod(x); }
Will it lead to the breaking changes?
It will work also with
{}types:function {count:number}::inc() { this.count++ }
The most difficult seems to parse the case where type should be enclosed into parentheses:
function (()=>void)::inc() { ... } function (A | B)::test() { ... }
And even
function<T> (T::(x: number)=>number)::bar(y: number, t: T): number { return t::this(y) + y } var foo = function {factor:number}::(x: number) { return this.factor*x*x; } console.log(foo::bar(5, {factor: 0.1})) // 7.5
However it seems that here we need not more than two or three look-ahead tokens. Need we?
Is there any official progress/comments on this?
Nate Higgins (@nathggns) see #3694 for the latest proposal
this is now tracked by #3694
- addedDuplicateAn existing issue was already createdAn existing issue was already created
on Sep 16, 2015 +1
- locked and limited conversation to collaborators
on Jun 18, 2018
This is an official proposal for one of the issues outlined in #229.
Firstly, I'd like to give a big thank you to Anders Hejlsberg (@ahejlsberg) for identifying the issue, and to Sergii Kliuchnyk (@redexp), Ivo Gabe de Wolff (@ivogabe), Andrew Bradley (@cspotcode), and others for their contributions to the original issue as they have formed most of the grunt work of this proposal. I've just decided I want the feature enough that I will take the time to write an official proposal.
I'd also like to note this is my first official proposal (for not just TypeScript, any language). I'm sorry if I've missed anything glaring out. Any advice on how to improve my proposals would be appreciated.
The Problem
When working with many libraries that take callbacks (including jQuery), TypeScript will give very little intelligence as to the type of the
thisArginside a callback's body �– intact, it will simply be typed asany.A few examples follow
Because the
thisArgis of typeany, we get no type checking and no autocomplete or any other kind of intellisense. Also, when defining functions within TypeScript user land code that rely on thethisArgbeing a certain type, there is nothing stopping consuming code calling it with whatever type they provide.Proposed Change
Ideally, a function definition would be able to define the type of its
thisArg. This means that any consuming code must provide athisArgof this type (usingcall,apply, orbind). It also means that within the body of that function, we can assume that the type of itsthisArgis of the provided type, rather than reverting to ananytype.Quick Note: None of this applies to functions defined using arrow functions. You cannot specify the type of the
thisArgwith arrow functions as their type is lexically bound. I believe if you try to use the syntax specified below with arrow functions it should be either a compiling error or a parsing error.There are many different places where you can define a function in TypeScript. However, the main syntax is as follows at the moment.
I propose adding to this syntax a way of specifying the type of the
thisArgwithin the function body.There are a few suggestions for this syntax including some from the original issue and some I have thought of myself, but I think the best out of the suggested is the following syntax:
In the statement and expression examples, we can assume that the type of
thisArginside their bodies isSomeThisType. In the last two examples, we can assume that type type ofthisArginside the passed callbacks is of typeSomeThisType.However, these assumptions rely on enforcing that any TypeScript code calling these functions have to pass the correct
thisArgs. Calling functions in JavaScript can get very complicated but here are the different ways that you can pass athisArg, along with the semantics of this proposal.Taken the following code:
We would no longer be able to call this function simply by invoking it, as the
thisArgwould be set to the global object instead of an instance ofSomeType. Trying to do so should throw a TypeError of some description (the exact error is intentionally left undecided at this point).Specifying the type of
thisArgusingcall,apply, orbindwith an incompatible type will throw the same error. Withcallandapplythe error is triggered at call time. Withbind, it is triggered at "bind time" (to more easily find the source of an error).Only invoking with a compatible type of
SomeTypewill be accepted by the type engine.Again, I would like to stress that this proposal has no affect on arrow functions. Even attempting to specify the
thisArgfor arrow functions should be an error.One notably exception to this is that you are able to pass arrow functions as arguments where a thisArg has been specified.
Example:
Note:
binddoes not change the specifiedthisArgof a function. This can only be set at define-time.The Syntax
You will have noticed that we pass the
thisArgwithin the type arguments of a function, where you would normally pass generic type information. This does not conflict however, as thethisArgmust be the last type argument, separated by a,if any other arguments are passed, and must be prefixed withthis:as follows.Note: This is a very contrived example, as very rarely will a function ever take a generic argument as well as specifying the
thisArgas specifying thethisArgis usually done when declaring function arguments and then passing anonymous functions like so:Why this syntax?
I chose this syntax as the main use case for this feature is for anonymous functions, in which you almost if not never have generic type arguments for. Alternatives were to add the this into the parameter list, which I disliked as it made for a messy-looking solution in the common use case.
Personally, I find the latter much harder to read.
How is this problem solved?
With this change, you can now specify the
thisArgwithin function bodies. While this works for any functions, the main use case is for passing anonymous functions to library code. With this change, you can now specify in a declaration thethisArgin the body of a function argument.This is a very basic example of how the on function could be written in a jQuery definition file. I've put the definition and usage in one file for convenience.
Note how in this example, the whole semantics of enforcing the type of
thisArgwhen calling a function is irrelevant as we're simply telling TypeScript how existing JavaScript works. This is actually the common use case for this code.Void thisArg
In some cases, the value of
thisArgcan be undefined (directly invoking a function in strict mode). It can be helpful to disallow consuming code from being able to use the value of this even if it isn't undefined, by making TypeScript assume it is undefined.You can do this like so:
Classes
Currently, explicitly setting the type of
thisArgin class methods is unsupported (triggering a syntax error) as doing this comes with its own set of problems, and can be added on at a later date once the basic semantics have been sorted.This also means you cannot dynamically change a method on a object to a function that has a specified
thisArg. Also, to maintain currently functionality, TypeScript will continue to assume that this is of the type in which the method is defined within the method body, but will allow you to trigger it with anythisArg.Automatically setting the thisArg within class methods
In the original issue, Andrew Bradley (@cspotcode) suggested that the
thisArgautomatically be set for class methods as such that the following is an error.I have intentionally left this out of this proposal as it causes a lot of its own backwards incompatibility issues, hence why there is no syntax addressed for setting the thisArg for class methods. However, should this proposal be accepted, then it might be worth extending it to address this. Should you choose to do this, this comment should be of some help to you.
Interfaces
While specifying a
thisArgtype for function interfaces is allowed, trying to do the same for methods or properties is not. This is something that can be revisited if/when class method support forthisArgtype specifications is added.Assignability
If
Bis assignable toA,function<this:B>is assignable tofunction<this:A>, but not vice versa.function<this:any>is assignable tofunction<this:A> andfunctionthis:B.functionthis:Aandfunctionthis:Bis assignable tofunctionthis:any`.Updating lib.d.ts
Andrew Bradley (@cspotcode) also showed that updating lib.d.ts to properly set the thisArg could cause a lot of breakage. Again, to simplify this proposal, I have left this out. Again, this is something I think should be addressed after choosing to merge this proposal, and this feature is still useful without updating
lib.d.ts.Emitted JavaScript
Regardless of the syntax used to specify the
thisArg, it must be stripped from the outputted JavaScript. This is simply a syntactical addition to the type engine, which is always stripped from emitted javascript.Incompatibilities
I won't claim to know of every feature currently proposed for ES6 and ES7 / current TypeScript proposals, but I don't believe that the proposed syntax conflicts any proposed feature.
Breaking Changes
There are no breaking changes by simply adding this feature (as functions must manually add an annotation for the type of their
thisArg). Breaking changes would only be introduced should this be added tolib.d.ts, or if classes begin to automatically set theirthisArg.