Skip to content

Supporting 'this' type #3694

Closed
Closed
@sophiajt

Description

@sophiajt

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

Metadata

Metadata

Assignees

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