Repository navigation
Proposal to improve static analysis for the thisArg of a function #1985
Description
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 thisArg inside a callback's body �– intact, it will simply be typed as any.
A few examples follow
// A standard jQuery example. While jQuery does
// also provide the value of `thisArg` as a argument
// to the callback, it is still helpful to provide a familiar example
$('body').on('click', function() {
this // any?
});
// The example from the original issue
// There would be no intellisense in this function, and TypeScript will
// just assume everything is okay even if we misspell get or Sammy's API changes
// because this is typed to any.
$.sammy('#view', function() {
this.get('#/', function() { ... });
});
// Here is a typescript example
// myClickHandler obviously relies on its thisArg being a HTMLElement
// but nothing stops code from calling it with any thisArg either
// directly (myClickHandler(...)) or with call/bind (myClickHandler.call(...))
function myClickHandler(event : MouseEvent) {
this.innerHTML = [event.pageX, event.pageY].join(', ')
}
document.body.addEventListener('click', myClickHandler);Because the thisArg is of type any, 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 the thisArg being 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 a thisArg of this type (using call, apply, or bind). It also means that within the body of that function, we can assume that the type of its thisArg is of the provided type, rather than reverting to an any type.
Quick Note: None of this applies to functions defined using arrow functions. You cannot specify the type of the thisArg with 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.
function someFunction(someArg : someType) : someResult { ... }I propose adding to this syntax a way of specifying the type of the thisArg within 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:
// Function Statement
function someFunction<this : SomeThisType>(someArg : SomeArgType) : SomeReturnType {
// this is of type SomeThisType
}
// Function expression
var someFunction = function<this : SomeThisType>(someArg : SomeArgType) : SomeReturnType {
// this is of type SomeThisType
}
// Function argument
// This would work similairly for declared functions (declare function ...) in definition files
function someFunction(someCallback : <this : SomeThisType>(someArg : SomeArgType) => SomeReturnType) : SomeReturnType { .. }
someFunction(function() {
// this is of type SomeThisType
});
// Interface function argument
interface SomeFunctionArgument {
<this : SomeThisType>(someArg : SomeArgType) : SomeReturnType;
}
function someFunction(someCallback : SomeFunctionArgument) : SomeReturnType { ... }
someFunction(function() {
// this is of type SomeThisType
});In the statement and expression examples, we can assume that the type of thisArg inside their bodies is SomeThisType. In the last two examples, we can assume that type type of thisArg inside the passed callbacks is of type SomeThisType.
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 a thisArg, along with the semantics of this proposal.
Taken the following code:
class SomeType {
doStuff() { ... }
}
var method = function<this : SomeType>() {
this.doStuff();
};We would no longer be able to call this function simply by invoking it, as the thisArg would be set to the global object instead of an instance of SomeType. Trying to do so should throw a TypeError of some description (the exact error is intentionally left undecided at this point).
method(); // Error: Window cannot be converted to SomeType ...Specifying the type of thisArg using call, apply, or bind with an incompatible type will throw the same error. With call and apply the error is triggered at call time. With bind, it is triggered at "bind time" (to more easily find the source of an error).
method.call({});
method.apply({});
method.bind({})();Only invoking with a compatible type of SomeType will be accepted by the type engine.
method.call(new SomeType());
method.apply(new SomeType());
method.bind(new SomeType())();Again, I would like to stress that this proposal has no affect on arrow functions. Even attempting to specify the thisArg for arrow functions should be an error.
var someFunction = <this : SomeType>() => { ... }; // ErrorOne notably exception to this is that you are able to pass arrow functions as arguments where a thisArg has been specified.
Example:
declare function someFunction(someCallback : <this : SomeType>() => number);
someFunction(() => 1);Note: bind does not change the specified thisArg of a function. This can only be set at define-time.
The Syntax
You will have noticed that we pass the thisArg within the type arguments of a function, where you would normally pass generic type information. This does not conflict however, as the thisArg must be the last type argument, separated by a , if any other arguments are passed, and must be prefixed with this: as follows.
class SomeType<T> {
constructor(public someProp : T) {}
}
function someFunction<T, this : SomeType<T>>() : T {
return this.someProp;
}
someFunction(); // Error
someFunction.call({}); // Error
someFunction.call(new SomeType<number>(1)); // 1Note: This is a very contrived example, as very rarely will a function ever take a generic argument as well as specifying the thisArg as specifying the thisArg is usually done when declaring function arguments and then passing anonymous functions like so:
declare someFunction(someCallback : <this : SomeType>() => number) : number;
someFunction(function() {
// this is of type SomeType
});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.
declare function someFunction<this : SomeType>(someArg : SomeArgType): any;
// vs
declare function someFunction(this : SomeType | someArg : SomeArgType): any;Personally, I find the latter much harder to read.
How is this problem solved?
With this change, you can now specify the thisArg within 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 the thisArg in 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 thisArg when 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.
// Definition
interface jQueryStatic {
(...any) : jQueryStatic; // Simple call definition.
on(eventName : string, callback : <this : HTMLElement>() => any) : jQueryStatic;
}
var $ : jQueryStatic;
// Usage
$('body').on('click', function() {
// this is now of type HTMLElement
});Void thisArg
In some cases, the value of thisArg can 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:
declare function someFunction(arg : <this : void>() => void);
someFunction(function() {
// this === undefined
});Classes
Currently, explicitly setting the type of thisArg in 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 any thisArg.
class Foo {
someMethod() {
// this is assumed to be of type Foo
}
}
var foo = Foo();
var method = foo.someMethod;
// All okay
foo.someMethod();
someMethod.call(foo);
someMethod();
someMethod.call({});
someMethod.apply({});
someMethod.bind({})();
// Not okay
foo.someMethod = function<this : number>() { .. };Automatically setting the thisArg within class methods
In the original issue, Andrew Bradley (@cspotcode) suggested that the thisArg automatically be set for class methods as such that the following is an error.
class SomeType {
someMethod() { ... }
}
(new SomeType()).someMethod.call({});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 thisArg type 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 for thisArg type specifications is added.
interface SomeFunctionInterface {
<this : number>(): number;
}
declare function numberMap(arr : number[], transformer : SomeFunctionInterface) : number[]; // Okay
interface SomeOtherInterface {
numberMapper : SomeFunctionInterface; // Error
}Assignability
If B is assignable to A, function<this:B> is assignable to function<this:A>, but not vice versa.
function<this:any> is assignable to function<this:A> andfunctionthis:B.functionthis:Aandfunctionthis:Bis assignable tofunctionthis:any`.
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>(){};
var a = function<this : any>(){};
x = y; // Error
y = x; // Error
zz = z; // Error
z = zz; // Okay
x = a; // Okay
y = a; // Okay
z = a; // Okay
zz = a; // Okay
a = x; // Okay
a = y; // Okay
a = z; // Okay
a = zz; // OkayUpdating 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 to lib.d.ts, or if classes begin to automatically set their thisArg.
Activity
Arnavion commentedon Feb 9, 2015
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:
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:
Is this allowed?
Since someMethod doesn't have an explicit thisArg nor an implicit thisArg per your proposal, it would seem it's allowed.
nathggns commentedon Feb 9, 2015
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:
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.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.
nathggns commentedon Feb 9, 2015
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).
jasonmeisel commentedon Feb 9, 2015
If the proposed syntax conflicts with JSX (which it seems like it would, but this should be verified), here are some other possibilities:
The biggest advantages of the proposed syntax are:
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.
cspotcode commentedon Feb 9, 2015
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)
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
nathggns commentedon Feb 9, 2015
For more examples of generics, look at the declarations for Q.js
nathggns commentedon Feb 9, 2015
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.react/react#759 (comment)
jasonmeisel commentedon Feb 9, 2015
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!
Arnavion commentedon Feb 9, 2015
Nate Higgins (@nathggns)
I do agree it should fail when calling bind. I just wanted to confirm that was your intent.
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, I misread your example. That said, by allowing them on the function operator you're also allowing them on interface and class methods. Consider:
Do you intend that func's thisArg type should revert to
anyin this case?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.nathggns commentedon Feb 10, 2015
Sorry do you mean the following?
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.Sorry I was being short-sited here. Yes, the assignment should error in order to stop changing Foo.
Arnavion commentedon Feb 10, 2015
Thanks, that answers my questions. You could put these points into your proposal post.
nathggns commentedon Feb 10, 2015
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.
Arnavion commentedon Feb 10, 2015
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.aholmes commentedon Feb 10, 2015
Nate Higgins (@nathggns)
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.nathggns commentedon Feb 10, 2015
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
28 remaining items
wizofaus commentedon Apr 15, 2015
"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.
cspotcode commentedon Apr 15, 2015
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.
Artazor commentedon Apr 16, 2015
For syntax I'd propose
And for generics:
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.kitsonk commentedon Jun 8, 2015
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:
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 commentedon Jun 10, 2015
Kitson 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:jussi-kalliokoski commentedon Jun 16, 2015
FWIW, 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):Artazor commentedon Aug 3, 2015
With respect to the new ES7 "::" bind operator I'd propose
Will it lead to the breaking changes?
Artazor commentedon Aug 3, 2015
It will work also with
{}types:The most difficult seems to parse the case where type should be enclosed into parentheses:
And even
However it seems that here we need not more than two or three look-ahead tokens. Need we?
nathggns commentedon Aug 6, 2015
Is there any official progress/comments on this?
danquirk commentedon Aug 6, 2015
Nate Higgins (@nathggns) see #3694 for the latest proposal
mhegazy commentedon Sep 16, 2015
this is now tracked by #3694
Zorgatone commentedon Sep 28, 2015
+1