Skip to content

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.

  1. Assignability checking for callbacks -- methods must match methods and lambdas must match lambdas.
  2. Objects built from standalone functions.
  3. 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:

  1. C#-like: function filter<T> (this: Iterable<T>, callback: (value: T) => boolean): Iterable<T>
  2. C++-like: function Iterable<T>::filter<T>(callback: (value: T) => boolean): Iterable<T>
  3. 'ES7-bind'-like: function filter<T> Iterable<T>::(callback: (value: T) => boolean): Iterable<T>
  4. this-type-parameter: function filter<T, this extends Iterable<T>>(callback: (value: T) => boolean): Iterable<T>

Semantics

The semantics fall into 3 areas

  1. Function body checking.
  2. Assignability checking.
  3. 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:

  • this is the class' this type for methods.
  • this is not bound for lambdas.
  • this is of type void for 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 object

Call-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 this for methods.
  • not present for lambdas.
  • of type void for 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

  1. How should loose-this work?

    1. No annotations allowed.
    2. No checking at call sites or assignments.
    3. Default to any or missing at call sites or assignments.
  2. Should function literal's this be 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 }
    }
  3. What should this of interface methods use?
    jeffreymorlan suggests using this for method-style declarations and void for 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 this parameters to interfaces, but they will mostly be desired ones.

  4. 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:

  1. Lambdas should work.
  2. Interfaces should work.
  3. Strict mode should actually be strict.
  4. Separate representation for this-parameters.
  5. Tests to ensure that type parameters and type guards work.
  6. Loose mode should work.

Activity

  1. falsandtru commented on Dec 9, 2015

    @falsandtru
    Contributor

    Can you add a following syntax?

    let o = {
      data: 12,
      ...
      h() {   // this is inferred from the contextual type
        console.log(this.data); 
      }
    }
  2. sandersn commented on Dec 9, 2015

    @sandersn
    MemberAuthor

    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.

  3. falsandtru commented on Dec 9, 2015

    @falsandtru
    Contributor

    Should I create an another issue?

  4. sandersn commented on Dec 9, 2015

    @sandersn
    MemberAuthor

    No, your example already works in Typescript, this just has type any. It's definitely part of this feature. I added your example to open question (2).

  5. falsandtru commented on Dec 9, 2015

    @falsandtru
    Contributor

    Thank you.

  6. kitsonk commented on Dec 10, 2015

    @kitsonk
    Contributor

    👍 for seeing a well thought out proposal! Yay!

  7. falsandtru commented on Dec 12, 2015

    @falsandtru
    Contributor

    I want an error for implicit any type of this using a compiler option. Implicit any type is dangerous.

  8. jeffreymorlan commented on Dec 12, 2015

    @jeffreymorlan
    Contributor

    How should interfaces work?
    Should the default be this? Missing? any? void?

    I think the best thing to do is default to this: this if using method syntax (foo(): string), default to this: void if 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 this always 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: void standalone functions. In particular, parameter interfaces often have callback functions which aren't necessarily called with the parameter object as this:

    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 of string, and x.bar has a type of () => string (not (this: this) => string or anything else).

    Same should go for object literals I guess: a() { ... } has a this type of the enclosing literal, a: function() { ... } has this: void. A contextual type should override it in at least the latter case, though.

  9. Artazor commented on Dec 14, 2015

    @Artazor
    Contributor

    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

  10. WebReflection commented on Dec 14, 2015

    @WebReflection

    I don't know if it's relevant for this case but regarding having a class with an onClick event and non accessible this, you might want to know or remember that since about ever you can define instead a handleEvent method which will automatically have the right this.

    class Handler {
      handleEvent(e) {
        let type = e.type;
        this['on' + type[0].toUpperCase() + type.slice(1)](e);
      }
    }

    Any instanceof Handler will be invoked with the right this as 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 this context issue.

    Best Regards

  11. Artazor commented on Dec 14, 2015

    @Artazor
    Contributor

    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 type any. 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?

  12. WebReflection commented on Dec 14, 2015

    @WebReflection

    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 Handler not so portable or worth, so that we are back to switch (event.type) within an handleEvent and 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

  13. rbuckton commented on Dec 15, 2015

    @rbuckton
    Contributor

    I am somewhat concerned about this in a parameter list as it could be forwards-incompatible should TC39 some day allow this in 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 this that 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 this in 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) { ... }
  14. 18 remaining items

  15. sandersn commented on Apr 8, 2016

    @sandersn
    MemberAuthor

    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.

  16. Igorbek commented on Apr 8, 2016

    @Igorbek
    Contributor

    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.

  17. saschanaz commented on Apr 8, 2016

    @saschanaz
    Contributor

    Did we have any great alternative syntax? I haven't seen one.

  18. Igorbek commented on Apr 8, 2016

    @Igorbek
    Contributor

    4 options listed here: #6018 (comment) by Anatoly Ressin (@Artazor). I'm fully for IV and III.

  19. gwicksted commented on Apr 8, 2016

    @gwicksted
    Contributor

    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.

  20. Artazor commented on Apr 9, 2016

    @Artazor
    Contributor

    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

  21. saschanaz commented on Apr 9, 2016

    @saschanaz
    Contributor

    Also, unfortunately the ES7 bind proposal is still on stage 0.

  22. Igorbek commented on Apr 9, 2016

    @Igorbek
    Contributor

    I have a question inspired by removed strictThisChecks option 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 = a shouldn't work, because a.f is of type (this: A) => void and i.f is of type (this: I) => void, and these types must be incompatible, however they are compatible at the moment.
    Actually we can replace this parameter to any formal parameter of type this and get same behavior on current bits.

  23. sandersn commented on Apr 11, 2016

    @sandersn
    MemberAuthor

    Igor Oleinikov (@Igorbek), I don't get an error with b.f = a.f unless I add a field to B, like private 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 type this:

    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.bind with 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.

  24. Igorbek commented on Apr 12, 2016

    @Igorbek
    Contributor

    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.

  25. Igorbek commented on Apr 12, 2016

    @Igorbek
    Contributor

    Nathan Shively-Sanders (@sandersn) thank you for clarification, that it's caused by parameter bivariance. But if we hadn't it, any usage of this type in class/interface hierarchy would make a derived class to be nonassignable to base type.

  26. farfromrefug commented on Apr 13, 2016

    @farfromrefug

    Would it make sense for module to return this in one of its methods?
    I am trying to build a ts doc for Titanium Mobile.
    In titanium you can create object like this

    let 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
    Titanium and UIare actually Proxy object so they have addEventListener method.
    As we can't say that a module implements we have to export the method for each module.
    But the problem is that the return type this won't work for modules.

    I understand that module are actually kind of a var, is there any way to export a function that returns the module itself?

    Or maybe there is another way to declare such a structure?

  27. sandersn commented on Apr 13, 2016

    @sandersn
    MemberAuthor

    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:

    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:

    1. this isn't a this-function type here, but a this-class type.
    2. Modules are now called namespaces. Standard Ecmascript modules are now called modules.
    3. 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?
    4. Since this a code design question, it's better suited to Stack Overflow than to a github design discussion.
  28. farfromrefug commented on Apr 14, 2016

    @farfromrefug

    Nathan Shively-Sanders (@sandersn) Thanks a lot will try that! And sorry will put that discussion in stackoverflow.
    Thanks

  29. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

FixedA PR has been merged for this issueSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions