Skip to main content

this Type_TypeScript Notes 11

Free2019-03-17#TypeScript#F-bounded polymorphism#有界多态性#TypeScript this type#TypeScript CRTP#TypeScript typeof this

Yes, this is also a type

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 to A<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

Reference Materials

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment