Description
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