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

Supporting 'this' type #3694

Closed
jonathandturner opened this issue Jul 1, 2015 · 73 comments

Comments

Projects
None yet
@jonathandturner
Copy link
Contributor

commented Jul 1, 2015

Background

We've had a few requests to support this typing (like #229). This proposal addresses the use cases for a function 'this' type, a class 'this' type, and a corresponding feature for interfaces.

Motivating Examples

Extension (lightweight mixin)

interface Model {
  extend<T>(t: T): <type of containing interface> & T;
}

interface NamedModel extends Model {
  name: string;
}

declare function createNameModel(): NamedModel;

var modelWithNameAndAge = createNamedModel().extend({
  age: 30
});
modelWithNameAndAge.name; //valid
modelWithNameAndAge.age; // valid

Safe instantiation

function f() {
  this.x = 3;
}
var g = new f(); // gives type an easy way to check f's body or specify instantiated type

Safe method invocation

function f() {
  this.x = 3;
}
var obj = {y: "bar", f: f};
obj.f(); // can’t easily error

Well-typed fluent APIs (also useful for cloning)

class Parent {
  setBase(property, value): <whatever the child class is> {
    this._secretSet(property, value);
    return this;
  }
}

class Child extends Parent { 
  name:string;
}

var c = new Child();
console.log(c.setBase("foo", 1).setBase("bar", 2").name);  //valid

Design

To support the motivating examples, we introduce a 'this' type. The 'this' type should follow the intuition of the developer. When used with classes, 'this' type refers to the type of the class it finds itself in. With functions, 'this' allows you to further document how the function will be used as a constructor and in what context it can be invoked while in an object.

Classes and 'this'

A class that uses a 'this' is referring to the containing named class. In the simplest example:

class C {
  myThis(): this { return this; }
}
var c = new C();
var d = c.myThis();

We trivially map the 'this' type in the invocation of myThis() to the C type, giving 'd' type C.

The type of 'this' will follow with subclasses. A subclass sees any 'this' in the type of its base class as its own type. This allows more a fluent API, as in this example:

class C {
  myThis(): this { return this; }
}
class D extends C {
  name: string;
}
var D = new D();
d.myThis().name = "Joe"; //valid

For this to work correctly, only 'this' or something that resolves to 'this' can be used. Something which looks like it should work correctly, but can't work safely is this example:

class C {
  newInstance(): this {
    return new C(); // error, 'this' only works with this expressions
  }
}

Functions and 'this'

Functions gain the ability to talk about the shape of the 'this' pointer that is visible in the function body. The design here leverages the type variables of the function to describe what the shape of 'this' has:

function f<this extends {x:number}>() {
  this.x = 3;
}

This is fairly readable, and we could use syntax coloring/tooling to help signify that the 'this' here is a special type variable that is implied by all functions.

Once the type of 'this' is described for functions, we can check the inside of the function body, where this is used:

function f<this extends {x:number}>() {
  this.x = 3;
  this.y = 6; //error: y is not available on this
}

We can also check invocation sites:

var o = {myMethod: f, y: "bob"};
o.myMethod(); //error: o does not match the shape of 'this' for 'myMethod'

We may even want to error on the assignment when object is first created instead of the invocation site.

Interfaces and 'this'

Similarly to classes, interfaces currently lack the the ability for a type to refer to itself. While an interfaces can refer to itself by name, this limits the ability of interfaces that extend the original interface. Here, we introduce 'this' as a way for interfaces to do this:

interface Model {
  clone(): this;
}

interface NamedModel extends Model {
  name: string;
}

var t:NamedModel;
t.clone().name; // valid

For this to work, 'this' refers to the containing named type. This helps eliminate ambiguities like this:

interface I {
  obj: { myself: this; name: string };
}

In this example, 'this' refers to I rather than the object literal type. It's trivial to refactor the object literal type out of the class so that you can describe a 'this' that instead binds to the object literal itself.

Motivating examples (Redux)

In this section, we re-write the motivating examples using the proposed functionality.

Extension (lightweight mixin)

interface Model {
  extend<T>(t: T): this & T;
}

interface NamedModel extends Model {
  name: string;
}

declare function createNameModel(): NamedModel;

var modelWithNameAndAge = createNamedModel().extend({
  age: 30
});
modelWithNameAndAge.name; //valid
modelWithNameAndAge.age; // valid

Safe instantiation

function f<this extends {x:number}>() {
  this.x = 3;
}
var g = new f(); // gives type an easy way to check f's body or specify instantiated type

Safe method invocation

function f<this extends {x: number}>() {
  this.x = 3;
}
var obj = {y: "bar", f: f};
obj.f(); // error: f can not be invoked on obj, missing {x: number}

Well-typed fluent APIs (also useful for cloning)

class Parent {
  setBase(property, value): this {
    this._secretSet(property, value);
    return this;
  }
}

class Child { 
  name:string;
}

var c = new Child();
console.log(c.setBase("foo", 1).setBase("bar", 2").name);  //valid
@DanielRosenwasser

This comment has been minimized.

Copy link
Member

commented Jul 1, 2015

Should methods now all implicitly gain a this type variable? This would prevent users from shooting themselves in the foot. For example:

class C {
    prop: number;
    method() {
        return this.x;
    }
}

let c = new C();
let removedMethodHandle = c.method;

// the following would error
console.log(removedMethodHandle());
@jonathandturner

This comment has been minimized.

Copy link
Contributor Author

commented Jul 1, 2015

@DanielRosenwasser possibly? Though I suspect that won't give you what you need. If the 'this' type shows up in the type of c.method, if you pass it to a callback that forgets the 'this' piece of the type, you won't catch the error.

@prabhjots

This comment has been minimized.

Copy link

commented Jul 1, 2015

'this' is a good idea. Also it could be applied to the 'call', 'bind' and 'apply' scenarios.

For Example:

class A {
    name: string;

    setName<this extends { name: string;}>(val:string) {
        this.name = val;
    }
}


class B {
    somethingElse: string;
 }

class C { name: string; }

var setName = new A().setName;
var b = new b();
var c = new C();
setName.apply(b, ["hello"]); //error
setName.call(b, "hello"); //error
setName.bind(b)("hello"); //error


setName.apply(c, ["hello"]); //ok
setName.call(c, "hello"); //ok
setName.bind(c)("hello"); //ok
@HerringtonDarkholme

This comment has been minimized.

Copy link
Contributor

commented Jul 3, 2015

👍 for the proposal

For the interface and class part, it seems this type overlaps a lot with F-bounded polymorphism. Can this type cover the latter?

@ahejlsberg

This comment has been minimized.

Copy link
Member

commented Jul 4, 2015

Some questions that come to mind...

(1) Your proposal doesn't specifically say, but I assume within a class this would denote a type that behaves like a subtype of the containing class (effectively like a type parameter with the current class as a constraint). That means we'd break the following code:

class C {
    foo() {
        var x = this;
        x = new C();
    }
}

(2) The proposal doesn't specify what the type of a method in a class is. For example:

class C {
    f() { }
}
var c = new C();
var f = c.f;  // Ideally of type <this extends C>() => void
f();          // Should be an error

the type of f currently is () => void, but I assume it would now be <this extends C>() => void. That would ensure f can't accidentally be called without a proper this.

(3) What are the type compatibility rules for function types with this type arguments? For example:

var c = new C();
var f: () => void = c.f;  // Is this allowed?

If we treat this as a type parameter, the above assignment would be allowed because we erase type parameters when checking assignment compatibility of signatures. That might in fact be the right thing to do because anything stricter would break a lot of existing code. But it is something we need to consider.

@DanielRosenwasser

This comment has been minimized.

Copy link
Member

commented Jul 4, 2015

@ahejlsberg, my comment brings up (2). From what I understand, @jonathandturner responded that because of your point in (3), we lose the benefit resulting from (2) because of cases where c.f is fed into a callback.

We could instead always take this constraints into account, and say that if an instance method never accesses this, it does not receive a this constraint. This way, we'd only break code in cases where a this handle was actually used, though, I could see it as slightly awkward to enforce this consistency.

@isiahmeadows

This comment has been minimized.

Copy link

commented Jul 7, 2015

Couldn't the above use case be covered by this?

interface Foo {
  bar(this: Foo & Bar): void;
}

interface Bar {
  bar(this: Foo | Bar): void
}

That, I believe, fits closer to the emerging syntax consensus in #229.

@DanielRosenwasser

This comment has been minimized.

Copy link
Member

commented Jul 7, 2015

@impinball could you clarify which use case you're referring to?

@isiahmeadows

This comment has been minimized.

Copy link

commented Jul 7, 2015

Sorry...I meant the this in function types in the main proposal. It wasn't specifically about your comment...probably should have mentioned @jonathandturner.

Also, as for that mixin model, there's already half a dozen mixin proposals out there, but this specific version proposed in the main bug is probably not the best way to go (feels incomplete).

@jonathandturner

This comment has been minimized.

Copy link
Contributor Author

commented Jul 7, 2015

@ahejlsberg - yeah, good to call out (1). Though I allude to it in the proposal by talking about assignment compat with 'this', I think the way you spell it out works better and gives a better intuition.

@DanielRosenwasser - We should explore implied 'this' constraint more, and try it out on some real world code to see what impact it might have.

@impinball - agreed. This isn't intended to be used as a complete mixin solution, rather a way of modeling extension like how some libraries (like Ember) do extensibility. For a full mixin story, you'd want the type-checking part (including something like intersection types) combined with a code generation piece.

@jonathandturner

This comment has been minimized.

Copy link
Contributor Author

commented Jul 17, 2015

@jbondc - do you have an example you think would cause problems?

@jonathandturner

This comment has been minimized.

Copy link
Contributor Author

commented Jul 17, 2015

I'm not sure exactly what your semantics are here, since this isn't valid JS/TS.

A related example in valid JS should show it a bit better:

class A extends mixin(B, C, D) {
   me(): this { return this } // includes me(), b(), c(), d, e?
}

So yeah, from the original example in the writeup a subclass would have a 'this' type that reflects the shape of the class. Unfortunately, I don't think there's enough descriptive power in the type system to cover the mixin forms.

@jonathandturner

This comment has been minimized.

Copy link
Contributor Author

commented Jul 17, 2015

(not to say there couldn't be in the future)

@isiahmeadows

This comment has been minimized.

Copy link

commented Jul 18, 2015

This also allows classical inheritance to be modeled
⭐️⭐️⭐️ correctly ⭐️⭐️⭐️.

It's not just a mixin problem.

(Yes, that amount of emphasis is necessary. I know of no other type system that fails at this, not even Java, C++, or C#. To me, the current behavior exposes a broken OO type system, and really is a bug in of itself.)

It's required to type libraries with deep object hierarchies. One such example, Lazy.js, cannot be typed without full type inheritance of delegated methods. It uses inheritance very frequently, with a very deep inheritance hierarchy. Its API uses a relatively deep hierarchy (although I also still need a way to override the return type of an inherited method for a couple specific cases in its API, namely the async equivalents of each). Lodash has a similar problem. Another example is the following code:

class A {
  foo(): this { return this }
}

class B extends A {
  bar(): this { return this }
}

new B().foo().bar()

If you change the above to this, it will fail to compile. That is surprising behavior for anyone used to most other static OO type systems.

class A {
  foo(): A { return this }
}

class B extends A {
  bar(): B { return this }
}

new B().foo().bar() // Property 'bar' does not exist on type 'A'.

This is kinda required for a few too many things.

@DanielRosenwasser

This comment has been minimized.

Copy link
Member

commented Jul 18, 2015

I know of no other type system that fails at this, not even Java, C++, or C#
...
That is surprising behavior for anyone used to most other static OO type systems.

I'm not sure what you're talking about, as far as I understand, none of those languages has self-typing.

If you change the above to this, it will fail to compile.

Yes, because this is currently under proposal.

@isiahmeadows

This comment has been minimized.

Copy link

commented Jul 18, 2015

The equivalent doesn't fail with Java 6 or later, correct? It's more about
the B still being a B, after being returned from an A.

On Sat, Jul 18, 2015, 14:27 Daniel Rosenwasser notifications@github.com
wrote:

I know of no other type system that fails at this, not even Java, C++, or
C#
...
That is surprising behavior for anyone used to most other static OO type
systems.

I'm not sure what you're talking about, as far as I understand, none of
those languages has self-typing.

If you change the above to this, it will fail to compile.

Yes, because this is currently under proposal.


Reply to this email directly or view it on GitHub
#3694 (comment)
.

@DanielRosenwasser

This comment has been minimized.

Copy link
Member

commented Jul 19, 2015

The equivalent doesn't fail with Java 6 or later, correct?

No, it fails in Java 8.

@MgSam

This comment has been minimized.

Copy link

commented Jul 19, 2015

class A {
  foo(): this { return this }
}

class B extends A {
  bar(): this { return this }
}

new B().foo().bar()

It seems to me that this should always fail to compile. Foo returns this, which is A. Why should parent class A know anything about B's method bar?

@kitsonk

This comment has been minimized.

Copy link
Contributor

commented Jul 19, 2015

It seems to me that this should always fail to compile. Foo returns this, which is A. Why should parent class A know anything about B's method bar?

While it is difficult to figure out at compile time, it is what is actually happening at runtime. Because this is simply a reference to the instance, means at runtime, the last statement is valid. So the argument is why couldn't TypeScript support this. This is valid ES6:

class A {
    foo() { return this }
}

class B extends A {
    bar() { return this }
}

new B().foo().bar();
@isiahmeadows

This comment has been minimized.

Copy link

commented Jul 19, 2015

Okay. I'll take back most of my claims about other languages. I didn't realize that they also had the same problem. I'll take another approach: could TypeScript finally get this right? It's runtime behavior that should also be possible to infer statically, without a lot of trouble.

@jonathandturner

This comment has been minimized.

Copy link
Contributor Author

commented Jul 20, 2015

@MgSam yup, it's what @kitsonk says. There's a difference here between 'this' type and returning the class type. Your example without the 'this' type would throw the error:

class A {
  foo(): A { return this }
}

class B extends A {
  bar(): B { return this }
}

new B().foo().bar()  // error Property 'bar' does not exist on type 'A'.

The 'this' typing is closer to what's happening at runtime, which is that B's instance is what is available after the new call.

@shovon

This comment has been minimized.

Copy link

commented Aug 27, 2015

I might point out that supporting the "this" type will be super helpful when using the bind operator.

It even looks like there are plans to support the bind operator in TypeScript, judging by the looks of 8521632.

@isiahmeadows

This comment has been minimized.

Copy link

commented Apr 8, 2016

@mhegazy I have a couple clarification questions:

  1. I'm guessing you're just referring to literal ES5 functions?
  2. Does func(function (x: string) { ... }) trigger noImplicitThis if the callee doesn't define a this type for the argument, such as in func(f: (x: string) => any): any)?
@sandersn

This comment has been minimized.

Copy link
Member

commented Apr 8, 2016

  1. Methods can also declare a this parameter:

    class C {
      totalCallback(this: void, e: Event); // assign to me!
    }
  2. noImplicitThis is triggered by this expressions in the body of a function. It is independent of assignability checks, which default to any if no this type is declared.

In other words, this is an implicit this error:

function f(n: number) {
  return this.m + n; // error -- this: any
}

But this is a normal old assignability error:

function f(this: void) { ... }
class C {
    m(this: this) { ... }
}
let c: C;
c.m = f; // error!
@isiahmeadows

This comment has been minimized.

Copy link

commented Apr 8, 2016

@sandersn Thanks! 😄

@vitaly-t

This comment has been minimized.

Copy link

commented May 9, 2016

@sandersn Thanks! I think the approach to declare the first function parameter as this: myType is the best one.

@nippur72

This comment has been minimized.

Copy link

commented Jun 21, 2016

IMO the syntax is a bit verbose, instead of

function f<this extends {x:number}>() {
  this.x = 3;
}

why not simply:

function f() {
  let this: {x:number};
  this.x = 3;
}
@Zorgatone

This comment has been minimized.

Copy link

commented Jun 21, 2016

@nippur72 no, that's not good. You're actually declaring a variable named this and not initializing it (undefined). The compiled code would have: var this; in it and shoud cause a runtime error

@nippur72

This comment has been minimized.

Copy link

commented Jul 15, 2016

has this landed in 2.0 ? I just copied this in a 2.0 project:

function f<this extends {x:number}>() {
  this.x = 3; 
}

but it tells me:

index.ts(1,12): error TS1139: Type parameter declaration expected.
index.ts(1,17): error TS1005: ';' expected.
index.ts(1,35): error TS1109: Expression expected.
index.ts(1,37): error TS1109: Expression expected.
index.ts(1,39): error TS1005: ';' expected.

Sorry, I see that the syntax is now

function f(this:{x:number}>() {
  this.x = 3; 
}
@shelby3

This comment has been minimized.

Copy link

commented Sep 10, 2016

@jonathandturner wrote:

class C {
  newInstance(): this {
    return new C(); // error, 'this' only works with this expressions
  }
}

How can polymorphic clone(): this be implemented if the above is not allowed? A class which implements the interface or abstract clone() method, could be extended by another class. It seems the compiler will need to deduce that inheritance from C types newInstance() as an abstract method even though the base has an implementation, so as to insure that each subclass provides a unique implementation.

function f<this extends {x:number}>() {
  this.x = 3;
  this.y = 6; //error: y is not available on this
}

Don't you mean implements instead of extends, since f is not allowed to add new member properties?

var o = {myMethod: f, y: "bob"};
o.myMethod(); //error: o does not match the shape of 'this' for 'myMethod'

I presume new o.myMethod() is legal.


@jonathandturner wrote:

@DanielRosenwasser wrote:

Should methods now all implicitly gain a this type variable? This would prevent users from shooting themselves in the foot. For example:

possibly? Though I suspect that won't give you what you need. If the 'this' type shows up in the type of c.method, if you pass it to a callback that forgets the 'this' piece of the type, you won't catch the error.

Google SoundScript's presentation says on page 24, "method extraction only allowed when this : any".

@ahejlsberg wrote:

class C {
    f() { }
}
var c = new C();
var f = c.f;  // Ideally of type <this extends C>() => void
f();          // Should be an error

the type of f currently is () => void, but I assume it would now be <this extends C>() => void. That would ensure f can't accidentally be called without a proper this.

That seems to be a nice generalization of the aforementioned plan for Google's SoundScript.

(3) What are the type compatibility rules for function types with this type arguments? For example:

var c = new C();
var f: () => void = c.f;  // Is this allowed?

If we treat this as a type parameter, the above assignment would be allowed because we erase type parameters when checking assignment compatibility of signatures. That might in fact be the right thing to do because anything stricter would break a lot of existing code. But it is something we need to consider.

That would break the aforementioned soundness expected by @DanielRosenwasser, which I see he pointed out.

class C {
    foo() {
        var x = this;
        x = new C();
    }
}

Why are you inferring the type of x: this instead of x: C?

@yhaskell

This comment has been minimized.

Copy link

commented Oct 5, 2016

Can we also propagate this to work with static functions?

Suppose, we have following code:

class C {
    constructor() { console.log("C"); }
    static foo() { return new this(); }
}

class D extends C { 
    constructor() {
        super();
        console.log("D");
    }
    bar() { return 42; }
}

var d = D.foo(); // valid and works. Will output C and then D

console.log(d instanceof D); // true

console.log((<D>d).bar()); // 42

Can we add a possibility to type it correctly?

@HerringtonDarkholme

This comment has been minimized.

Copy link
Contributor

commented Oct 5, 2016

Of course you can. TypeScript has very advanced type system now.

class C {
  static foo = function <D extends C>(this: {new(): D}) {return new this()}
}

class D extends C {
  bar() { return 42}
}

var d = D.foo()
d.bar()
@yhaskell

This comment has been minimized.

Copy link

commented Oct 5, 2016

That looks like very dirty hack, although it works.

@marcusjwhelan

This comment has been minimized.

Copy link

commented Apr 18, 2017

Is it possible to get the Type of my class I am currently working in? for example

export class A{
    // constructor + methods
    // can use `A` as a return Type
}
export class B extends A{
    public b:any = this;
    // here in my methods if I use a return type of `B` it implies class `A`type
    // how do I use `B` as a type?
    fun():B {
        const self = this.b;
        let hmm:B;  // Variable in question

        // Logic to fill variable hmm with self like item

        return hmm;  // Error: Type 'A' is not assignable to type 'B' , property 'some class var' is not in type 'A.
    }
}
@basarat

This comment has been minimized.

Copy link
Contributor

commented Apr 18, 2017

@marcusjwhelan I think you want:

export class A {
}
export class B extends A {
    fun(): this {
        return this;
    }
}

https://egghead.io/lessons/typescript-create-a-fluent-api-using-typescript-classes 🌹

@marcusjwhelan

This comment has been minimized.

Copy link

commented Apr 18, 2017

@basarat ah thank you for the quick reply. Although I actually figured out that it was fault code on my part. The reason I could not use the class as a type was an issue with me assigning the variable that was to be returned as the parent type, copy paste error.

@etsuo etsuo referenced this issue Dec 30, 2017

Closed

Update how Decorator Typing Works #111

3 of 3 tasks complete

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
You can’t perform that action at this time.