Skip to content

Proposal to improve static analysis for the thisArg of a function #1985

Closed
@nathggns

Description

@nathggns

This is an official proposal for one of the issues outlined in #229.

Firstly, I'd like to give a big thank you to @ahejlsberg for identifying the issue, and to @redexp, @ivogabe, @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>() => { ... }; // Error

One 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)); // 1

Note: 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, @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; // Okay

Updating lib.d.ts

@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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already createdSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions