1. this is Also a Type!
class BasicDOMNode {
constructor(private el: Element) { }
addClass(cssClass: string) {
this.el.classList.add(cssClass);
return this;
}
}
class DOMNode extends BasicDOMNode {
addClasses(cssClasses: string[]) {
for (let cssClass of cssClasses) {
this.addClass(cssClass);
}
return this;
}
}
Among them, addClass and addClasses type signatures are respectively:
addClass(cssClass: string): this
addClasses(cssClasses: string[]): this
Return type is this, indicates subtype of containing class or interface (called F-bounded polymorphism), for example:
let node = new DOMNode(document.querySelector('div'));
node
.addClass('page')
.addClasses(['active', 'spring'])
.addClass('first')
In above chain call, this type can automatically correspond to containing class instance type. Yes, this JavaScript runtime feature, in TypeScript static type system is also supported
Specifically, this types in TypeScript are divided into 2 categories:
-
class this type: this type in class/interface (member methods)
-
function this type: this type in normal functions
2. Class this type
this in JavaScript Class
// JavaScript
class A {
foo() { return this }
}
class B extends A {
bar() { return this }
}
new B().foo().bar();
Chain call in above example will execute normally, finally returns B class instance. We know at runtime this points to current class or its subclass instance, this is a very common behavior in JavaScript runtime
That is, this's type is not fixed, depends on its call context, for example:
// A class instance type
new A().foo();
// B class instance type
new B().foo();
// B class instance type
new A().foo.call(new B());
this in Class A doesn't always point to A class instance (could also be A's subclass instance), then, how should we describe this's type?
this's Type
If we want to add type description to initial scenario, we might try like this (if without class this type):
declare class A {
foo(): A;
}
declare class B extends A {
bar(): B;
}
// Error Property 'bar' does not exist on type 'A'.
new B().foo().bar();
Expected result, foo(): A returns A class instance, of course can't find subclass B's member methods. What's actually expected is:
A class instance type, has foo() method
|
new B().foo().bar()
|
B class instance type, has bar() method
Then, further try:
declare class A {
foo(): A & B;
}
declare class B extends A {
bar(): B & A;
}
new B().foo().bar();
this in B class is both B class instance and A class instance, let's assume bar(): B & A is appropriate, but anyway foo(): A & B is unreasonable, because base class instance is not necessarily subclass instance... We seem to have no way to mark a suitable type for this, especially in superThis.subMethod() scenario
Therefore, for similar scenarios, necessary to introduce a special type, i.e. this type:
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).
this type behaves as subtype of containing class/interface, this is consistent with JavaScript runtime this value mechanism, for example:
class A {
foo(): this { return this }
}
class B extends A {
bar(): this { return this }
}
new B().foo().bar()
That is, this type is this value's type:
In a non-static member of a class or interface, this in a type position refers to the type of this.
Implementation Principle
The polymorphic this type is implemented by providing every class and interface with an implied type parameter that is constrained to the containing type itself.
In short, treat class/interface as generic with implicit type parameter this, and add type constraints related to its containing class/interface
Consider every class/interface as a generic type with an implicit this type arguments. The this type parameter is constrained to the type, i.e.
A<this extends A<A>>. The type of the value this inside a class or an interface is the generic type parameter this. Every reference to class/interface A outside the class is a type reference toA<this: A>. assignment compatibility flows normally like other generic type parameters.
Specifically, this type in implementation is equivalent to A<this extends A<A>> (i.e. classic CRTP Curiously Recurring Template Pattern), this value's type in class is generic parameter this. Outside current class/interface context, this's type is A<this: A>, type compatibility etc. consistent with generics
So, this type is like an implicit [type parameter](/articles/泛型-typescript 笔记 6/#articleHeader3) with class derivation relationship [constraint](/articles/泛型-typescript 笔记 6/#articleHeader8)
3. Function this type
Besides class/interface, this type also applies to normal functions
Different from class this type usually plays role implicitly (such as automatic [type inference](/articles/深入类型系统-typescript 笔记 8/#articleHeader1)), function this type mostly through explicit declaration to constrain this value's type in function body:
This-types for functions allows Typescript authors to specify the type of this that is bound within the function body.
Implementation Principle
Treat this explicitly as function's (first) parameter, thereby constrain its type, perform type checking like normal parameters. For example:
declare class C { m(this: this); }
let c = new C();
// f type is (this:C) => any
let f = c.m;
// Error The 'this' context of type 'void' is not assignable to method's 'this' of type 'C'.
f();
Note, only check when explicitly declared this value type (as above example):
// Remove explicitly declared this type
declare class C { m(); }
let c = new C();
// f type is () => any
let f = c.m;
// Correct
f();
P.S. Specially, arrow function (lambda)'s this cannot manually constrain its type:
let obj = {
x: 1,
// Error An arrow function cannot have a 'this' parameter.
f: (this: { x: number }) => this.x
};
Association with class this type
Member methods are also functions, two this types intersect here:
If this is not provided, this is the class' this type for methods.
That is, in member methods, if function this type is not provided, then continue using that class/interface's class this type, similar to relationship between automatically inferred type and explicitly declared type: latter can override former
Note, although original design was like this (enable strictThis/strictThisChecks option), but due to performance and other reasons, later removed this option. Therefore, currently function this type and class this type implicit checking are both very weak (for example member methods without explicitly specified this type don't default to having class this type constraint)
class C {
x = { y: 1 };
f() { return this.x; }
}
let f = new C().f;
// Correct
f();
Among them f's type is () => { y: number; }, not expected (this: C) => { y: number; }
4. Application Scenarios
Fluent Interface
this type makes fluent interface very easy to describe, for example:
class A {
foo(): this { return this }
}
class B extends A {
bar(): this { return this }
}
new B().foo().bar()
P.S. So-called fluent interface (design level), can simply understand as chain call (implementation level):
A fluent interface is a method for designing object oriented APIs based extensively on method chaining with the goal of making the readability of the source code close to that of ordinary written prose, essentially creating a domain-specific language within the interface.
(From Fluent interface)
In short, fluent interface is an API design method in OOP, through chain method calls makes source code highly readable
Describe this's Type
function this type allows us to constrain this's type like describing normal parameters, this is especially important in Callback scenarios:
class Cat {
constructor(public name: string) {}
meow(this: Cat) { console.log('meow~'); }
}
class EventBus {
on(type: string, handler: (this: void, ...params) => void) {/* ... */}
}
// Error Argument of type '(this: Cat) => void' is not assignable to parameter of type '(this: void, ...params: any[]) => void'.
new EventBus().on('click', new Cat('Neko').meow);
(From [this 的类型](/articles/函数-typescript 笔记 5/#articleHeader8))
Track context Type
With this type, bind, call, apply etc. scenarios can also correctly maintain type constraints, requires current function this consistent with passed target object type:
apply<T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, args: A): R;
call<T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T, ...args: A): R;
bind<T, A extends any[], R>(this: (this: T, ...args: A) => R, thisArg: T): (...args: A) => R;
Let similar errors expose (need to enable strictBindCallApply option):
class C {
constructor(a: number, b: string) {}
foo(this: this, a: number, b: string): string { return "" }
}
declare let c: C;
let f14 = c.foo.bind(undefined); // Error
let c14 = c.foo.call(undefined, 10, "hello"); // Error
let a14 = c.foo.apply(undefined, [10, "hello"]); // Error
P.S. For more information about bind, call, apply etc. type constraints, see Strict bind, call, and apply methods on functions
No comments yet. Be the first to share your thoughts.