
The polymorphic this type
In JavaScript, the value of the this operator is determined by the way a function or method is invoked. In a method, the this operator usually points to the class instance.
The polymorphic this type is an improved version of the original type inference for the this operator that introduced the following behavior as documented by Anders Hejlsberg:
- The type of this in an expression within a nonstatic class or interface member is an instance of some class that derives from the containing class, as opposed to simply an instance of the containing class.
- The this keyword can be used in a type position within a nonstatic class or interface member to reference the type of this.
- When a class or interface is referenced as a type, all occurrences of the this type within the class (including those inherited from base classes) are replaced with the type itself.
This feature makes patterns such as fluent interfaces (https://en.wikipedia.org/wiki/Fluent_interface) much easier to express and implement, as we can see in the following example:
interface Person {
name?: string;
surname?: string;
age?: number;
}
class PersonBuilder<T extends Person> {
protected _details: T;
public constructor() {
this._details = {} as T;
}
public currentValue(): T {
return this._details;
}
public withName(name: string): this {
this._details.name = name;
return this;
}
public withSurname(surname: string): this {
this._details.surname = surname;
return this;
}
public withAge(age: number): this {
this._details.age = age;
return this;
}
}
Since the class methods return the this type, we can invoke multiple methods without having to write the class name multiple times:
let value1 = new PersonBuilder()
.withName("name")
.withSurname("surname")
.withAge(28)
.currentValue();
Since the class uses the this type, we can extend it and the new class can then provide a fluent interface that includes the base methods as well:
interface Employee extends Person {
email: string;
department: string;
}
class EmployeeBuilder extends PersonBuilder<Employee> {
public withEmail(email: string) {
this._details.email = email;
return this;
}
public withDepartment(department: string) {
this._details.department = department;
return this;
}
}
let value2 = new EmployeeBuilder()
.withName("name")
.withSurname("surname")
.withAge(28)
.withEmail("name.surname@company.com")
.withDepartment("engineering")
.currentValue();