Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Function this types #6018

Closed
sandersn opened this issue Dec 9, 2015 · 40 comments
Closed

Function this types #6018

sandersn opened this issue Dec 9, 2015 · 40 comments
Assignees
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@sandersn
Copy link
Member

sandersn commented Dec 9, 2015

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.

@Artazor and @rbuckton point out that this as argument may not be forward-compatible with future versions of Ecmascript. @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.
@sandersn sandersn added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Dec 9, 2015
@falsandtru
Copy link
Contributor

Can you add a following syntax?

let o = {
  data: 12,
  ...
  h() {   // this is inferred from the contextual type
    console.log(this.data); 
  }
}

@sandersn
Copy link
Member Author

sandersn commented Dec 9, 2015

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.

@falsandtru
Copy link
Contributor

Should I create an another issue?

@sandersn
Copy link
Member Author

sandersn commented Dec 9, 2015

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

@falsandtru
Copy link
Contributor

Thank you.

@kitsonk
Copy link
Contributor

kitsonk commented Dec 10, 2015

👍 for seeing a well thought out proposal! Yay!

@falsandtru
Copy link
Contributor

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

@jeffreymorlan
Copy link
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.

@Artazor
Copy link
Contributor

Artazor commented Dec 14, 2015

@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 @WebReflection, @bergus, @zenparsing, @rbuckton while trying to align with ES7 bind operator :: and #3508

@WebReflection
Copy link

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

@Artazor
Copy link
Contributor

Artazor commented Dec 14, 2015

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

@WebReflection
Copy link

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

@rbuckton
Copy link
Member

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. @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) { ... }

@saschanaz
Copy link
Contributor

bound<Iterable<T>> function filter<T>(callback: (value: T) => boolean): Iterable<T> { ... }
bound<Iterable<T>> (callback: (value: T) => boolean): Iterable<T> => { ... }
function filter<T>(callback: (value: T) => boolean) binds Iterable<T> returns Iterable<T> { ... }
(callback: (value: T) => boolean) binds Iterable<T> returns Iterable<T> => { ... }

Just a brainstorming.

@kitsonk
Copy link
Contributor

kitsonk commented Dec 15, 2015

@rbuckton I get what you are saying about this in the parameter list, though when I first read it, I thought you were just afraid that TC39 would actually do something, though what I think you are trying to express is that, syntactically, the aim is not to play around in the space that is the syntax of JavaScript, especially because it needs to be fully erasable.

I think of trying to align to the potential bind operator (::) could also lead to lots of confusion. Just like using typeof and instanceof in both TS and ES land, but with very different meanings.

Of the suggestions you made, I found the one in the type parameter list the best in my opinion, but with one variation:

function filter<T, this:Iterable<T>>(callback: (value: T) => boolean): Iterable<T> { ... }

The only problem I see with that or your suggestion though, is that it seems to imply that two arguments are required on the invocation of the function:

filter<string, ?>(cb);

Also, how would the following be dealt with:

function filter<T, this extends Iterable<T>, U>(callback: (value: T) => boolean, options: U): Iterable<T> { ... }

@Artazor
Copy link
Contributor

Artazor commented Dec 15, 2015

@rbuckton

...it looks awkward, especially when you try to apply it to a function declaration

I think it's rather subjective. I believe that this solution has several advantages

  1. It doesn't conflates formal parameters;
  2. It doesn't conflates formal type parameters and doesn't violates current restriction: Constraint of a type parameter cannot reference any type parameter from the same type parameter list;
  3. It uses type parameter natural order: introduce, then use;
  4. It is dual with the :: usage ThisArg::FuncType <=> obj::fn

Let's list all alternatives

I. this as function parameter

function filter<T>(this: Iterable<T>, callback: (value: T) => boolean): Iterable<T> { ... }

Pros: conforms 2, 3, minimal efforts for parsing
Cons: violates 1, 4

II. this as function generic type parameter

function filter<T, this extends Iterable<T>>(callback: (value: T) => boolean): Iterable<T> { ... }

Pros: conforms 1, 3
Cons: violates 2, 4

III. C++ inspired variant

function Iterable<T>::filter<T>(callback: (value: T) => boolean): Iterable<T> { ... }

Pros: conforms 1, 2
Cons: violates 3, 4

IV. ES7-bind inspired variant

function filter<T> Iterable<T>::(callback: (value: T) => boolean): Iterable<T> { ... }

Pros: conforms 1, 2, 3, 4
Cons: significant parsing rules changes (disambiguation);


My preferences in decreasing order are: IV, I, III, II

@sandersn
Copy link
Member Author

@falsandtru this types are planned to be behind a flag that prevents implicit any. It will not be the default, however, because this has defaulted to any in too much existing code.

@jeffreymorlan the syntax distinction does solve the problem nicely. It's a bit subtle but it might be needed in this case just to be usable. I'm afraid the syntax distinction would end up in tons of posts like "12 Secrets of Typescript Exposed". @ahejlsberg do you have an opinion on this?

@rbuckton, @Artazor thanks for the detailed suggestions on syntax. I'll add them to the proposal when I have time. As you can see I hadn't thought too much about it previously--I just used what was convenient for prototyping.

@Think7
Copy link

Think7 commented Dec 16, 2015

Disclaimer: I am not sure if this adds to the discussion or is about the same thing. i may have missed the point entirely. I apologize in advance it this is the case!

The below example DOES work as expected though despite the TS compiler complaining about it. The idea here is for the compiler to know that Person.fromData() returns a Person instance rather than an any or Base instance.

class Base {
    protected static deserialize<T>(aClass: { new(...args: any[]): T }, data: Object): T {
        let instModel = new aClass();
        let keys = Object.keys(data);

        for (let i = 0; i < keys.length; i++) {
            let cKey = keys[i];
            instModel[cKey] = data[cKey];
        }

        return <T>instModel;
    }
}

class Model extends Base {
    private _id: ObjectID;

    constructor() {
        super();
        this._id = new ObjectID();
    }
}

class Person extends Model {
    private _firstName: string;
    private _lastName: string;

    public get firstName(): string { return this._firstName; }
    public set firstName(firstName: string) { this._firstName = firstName; }

    public get lastName(): string { return this._lastName; }
    public set lastName(lastName: string) { this._lastName = lastName; }

    public static fromData(data: Object): this {
        return this.deserialize(this, data);
    }
}

let data = {
    firstName: "Malcolm",
    lastName: "Reynolds"
};

let aCaptian = Person.fromData(data);
console.log(aCaptian instanceof Person); // True
console.log(aCaptian.firstName); // Malcolm
console.log(aCaptian.lastName); // Reynolds

Error:

Error:(44, 43) TS2526: A 'this' type is available only in a non-static member of a class or interface.

For me personally, having the return type of "this" in the Person.fromData() method works well. Since the "this" referenced in the fromData method represents the Person class itself, the compiler should know that a return type of "this" means an object of the Person type is being returned.

Similarly, "this" as an input param to the Base.deserialize method also works well as again, the this represents the Person class which is then new'd.

@sandersn
Copy link
Member Author

@Think7, you're working with the classes' this type, which is already in Typescript. It's not related to specifying this inside a function body.

@gwicksted
Copy link
Contributor

IV. ES7-bind inspired variant is also my preference.

// Inline arrow function with this type specified:
function filter<T> Iterable<T>::(callback: Iterable<T>::(value: T) => boolean): Iterable<T> { ... }

// Arrow function type alias with this type specified:
export type FilterCallback<T> = Iterable<T>::(value: T) => boolean;
function filter<T> Iterable<T>::(callback: FilterCallback<T>): Iterable<T> { ... }

// Overriding previously defined this type inline:
function filter<T> Iterable<T>::(callback: SuperIterable<T>::FilterCallback<T>): Iterable<T> { ... }

// Function this type declaration:
export type MyCallback<T> = Iterable<T>::Function;

I can only assume there will be corresponding compiler flag(s): implicit any this vs implicit class this. And hopefully no implicit this will be absent!

-1 for option II since it implies specialization might be available via type of this and I assume that is not possible without runtime dispatching based on type of this so probably not a desirable language feature.

@mhegazy
Copy link
Contributor

mhegazy commented Apr 7, 2016

Fixed by #6739

@mhegazy mhegazy closed this as completed Apr 7, 2016
@mhegazy mhegazy added Fixed A PR has been merged for this issue and removed In Discussion Not yet reached consensus labels Apr 7, 2016
@mhegazy mhegazy added this to the TypeScript 2.0 milestone Apr 7, 2016
@Igorbek
Copy link
Contributor

Igorbek commented Apr 7, 2016

Could we discuss the syntax? The merged PR has adopted option I (from @Artazor) which is a first parameter named this. I'm personally strongly against that option and believe the majority here also preferred other options.
For me, this in parameter position confuses people and will lead to misinterpreting.
My preferences in decreasing order are: IV, III and strongly against I and II.

@mattmccutchen
Copy link
Contributor

Do I understand correctly that with the removal of --strictThis from the current iteration of the change and the readdition of --noImplicitThis, we now have all the new function body checking but none of the new call site or assignability checking? @mhegazy appeared to say there is call site and assignability checking, but if so, I'd expect a compile error in the following, which I don't get as of b9c4b02:

class MyClass {
  public x = {y: 45};
  blah() {
    console.log(this.x.y);
  }
}

let m = new MyClass();
let f = m.blah;
f();

Is there a follow-up issue for the call site and assignability checking?

@mhegazy
Copy link
Contributor

mhegazy commented Apr 8, 2016

Is there a follow-up issue for the call site and assignability checking?

no, feel free to file one

Also note that explicitly specifying the this argument results in the correct error. the issue is about implicitly doing this for all classes and interfaces. i.e.:

class MyClass {
  public x = {y: 45};
  blah(tihs:this) {
    console.log(this.x.y);
  }
}
....

f(); // error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.

@mattmccutchen
Copy link
Contributor

@mhegazy, thanks for the explanation; it saved me a while digging through comments and/or code So if I specify the this type on all the signatures involved in a call or assignment, that call or assignment is fully checked, and we are just missing the flag to enforce the checking on the whole program?

@gwicksted
Copy link
Contributor

@Igorbek at least it's easy to type and read. I'm thinking they chose option 1 for those less accustomed to JavaScript.

@sandersn
Copy link
Member Author

sandersn commented Apr 8, 2016

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

@Igorbek
Copy link
Contributor

Igorbek commented Apr 8, 2016

@gwicksted it's easy to type but it's definitely not easy to read. It's so easy to mess with formal parameter - it would need to know that would be a special case and at call site that argument wouldn't need to be passed.
I love the feature and was really looking forward to use it, but the syntax seems very unclear. I urge these who interested/participated in the discussion to reiterate your feeling about the syntax again.

@saschanaz
Copy link
Contributor

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

@Igorbek
Copy link
Contributor

Igorbek commented Apr 8, 2016

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

@gwicksted
Copy link
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.

@Artazor
Copy link
Contributor

Artazor commented Apr 9, 2016

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 @Igorbek we have little chances to make any influence on it. It is the battle: simplicity versus purity. Simplicity wins

@saschanaz
Copy link
Contributor

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

@Igorbek
Copy link
Contributor

Igorbek commented Apr 9, 2016

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.

@sandersn
Copy link
Member Author

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

@Igorbek
Copy link
Contributor

Igorbek commented Apr 12, 2016

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.

@Igorbek
Copy link
Contributor

Igorbek commented Apr 12, 2016

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

@farfromrefug
Copy link

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?

@sandersn
Copy link
Member Author

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.

@farfromrefug
Copy link

@sandersn Thanks a lot will try that! And sorry will put that discussion in stackoverflow.
Thanks

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests