this类型_TypeScript笔记11

一.this也是一种类型!

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;
  }
}

其中,addClassaddClasses的类型签名分别是:

addClass(cssClass: string): this
addClasses(cssClasses: string[]): this

返回类型是this,表示所属类或接口的子类型(称之为有界多态性(F-bounded polymorphism)),例如:

let node = new DOMNode(document.querySelector('div'));
node
  .addClass('page')
  .addClasses(['active', 'spring'])
  .addClass('first')

上面的链式调用中,this类型能够自动对应到所属类实例类型上。没错,这种JavaScript运行时特性,在TypeScript静态类型系统中同样支持

具体地,TypeScript中的this类型分为2类:

  • class this type:类/接口(的成员方法)中的this类型

  • function this type:普通函数中的this类型

二.Class this type

JavaScript Class中的this

// JavaScript
class A {
  foo() { return this }
}
class B extends A {
  bar() { return this }
}

new B().foo().bar();

上例中的链式调用会正常执行,最后返回B类实例。我们知道运行时this指向当前类或其子类实例,这在JavaScript运行时是一种非常常见的行为

也就是说,this的类型并不是固定的,取决于其调用上下文,例如:

// A类实例类型
new A().foo();
// B类实例类型
new B().foo();
// B类实例类型
new A().foo.call(new B());

Class A中的this并不总是指向A类实例(也有可能是A的子类实例),那么,应该如何描述this的类型?

this的类型

要给最初的场景添上类型描述的话,我们可能会这样尝试(如果没有class this type):

declare class A {
  foo(): A;
}
declare class B extends A {
  bar(): B;
}

// 错误 Property 'bar' does not exist on type 'A'.
new B().foo().bar();

意料之中的结果,foo(): A返回A类实例,当然找不到子类B的成员方法。实际期望的是:

   A类实例类型,具有foo()方法
       |
new B().foo().bar()
             |
         B类实例类型,具有bar()方法

那么,进一步尝试:

declare class A {
  foo(): A & B;
}
declare class B extends A {
  bar(): B & A;
}

new B().foo().bar();

B类中的this既是B类实例也是A类实例,姑且认为bar(): B & A是合适的,但无论如何foo(): A & B是不合理的,因为基类实例并不一定是子类实例……我们似乎没有办法给this标出一个合适的类型,尤其是在superThis.subMethod()的场景

因此,针对类似的场景,有必要引入一种特殊的类型,即this类型:

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类型表现为所属类/接口的子类型,这与JavaScript运行时的this值机制一致,例如:

class A {
  foo(): this { return this }
}
class B extends A {
  bar(): this { return this }
}

new B().foo().bar()

也就是说,this类型就是this值的类型:

In a non-static member of a class or interface, this in a type position refers to the type of this.

实现原理

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.

简言之,就是把类/接口看作具有隐式类型参数this的泛型,并加上其所在类/接口相关的类型约束

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.

具体的,this类型在实现上相当于A<this extends A<A>>(即经典的CRTP 奇异递归模板模式),类中this值的类型就是泛型参数this。出了当前类/接口的上下文,this的类型就是A<this: A>,类型兼容性等与泛型一致

所以,this类型就像一个带有类派生关系约束的隐式类型参数

三.Function this type

除了类/接口外,this类型还适用于普通函数

不同于class this type通常隐式发挥作用(如自动类型推断),function this type大都通过显式声明来约束函数体中this值的类型:

This-types for functions allows Typescript authors to specify the type of this that is bound within the function body.

实现原理

this显式地作为函数的(第一个)参数,从而限定其类型,像普通参数一样进行类型检查。例如:

declare class C { m(this: this); }
let c = new C();
// f 类型为 (this:C) => any
let f = c.m;
// 错误 The 'this' context of type 'void' is not assignable to method's 'this' of type 'C'.
f();

注意,仅在显式声明了this值类型时才进行检查(如上例):

// 去掉显式声明的this类型
declare class C { m(); }
let c = new C();
// f 类型为 () => any
let f = c.m;
// 正确
f();

P.S.特殊的,箭头函数(lambda)的this无法手动限定其类型:

let obj = {
  x: 1,
  // 错误 An arrow function cannot have a 'this' parameter. 
  f: (this: { x: number }) => this.x
};

与class this type的关联

成员方法同时也是函数,两种this类型在这里产生了交集:

If this is not provided, this is the class’ this type for methods.

也就是说,成员方法中,如果没提供function this type,那么就沿用该类/接口的class this type,类似于自动推断而来的类型与显式声明类型之间的关系:后者能够覆盖前者

注意,虽然最初的设计是这样的(开启strictThis/strictThisChecks选项),但由于性能等方面的原因,后来去掉了该选项。因此,目前function this type与class this type隐式检查都很弱(比如未显式指定this类型的成员方法并不默认具有class this type约束)

class C {
  x = { y: 1 };
  f() { return this.x; }
}

let f = new C().f;
// 正确
f();

其中f的类型是() => { y: number; },而不是预期的(this: C) => { y: number; }

四.应用场景

流式接口(Fluent interface)

this类型让流式接口(fluent interface)变得很容易描述,例如:

class A {
  foo(): this { return this }
}
class B extends A {
  bar(): this { return this }
}

new B().foo().bar()

P.S.所谓的流式接口(设计层面),可以简单理解为链式调用(实现层面)

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.

(摘自Fluent interface

简言之,流式接口是OOP中的一种API设计方式,通过链式方法调用让源码极具可读性

描述this的类型

function this type允许我们像描述普通参数一样限定this的类型,这在Callback场景尤为重要:

class Cat {
  constructor(public name: string) {}
  meow(this: Cat) { console.log('meow~'); }
}

class EventBus {
  on(type: string, handler: (this: void, ...params) => void) {/* ... */}
}

// 错误 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);

(摘自this的类型

追踪context类型

有了this类型,bindcallapply等场景也能正确维持类型约束,要求当前函数this与传入的目标对象类型一致:

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;

让类似的错误暴露出来(需要开启strictBindCallApply选项):

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.关于bindcallapply等类型约束的更多信息,见Strict bind, call, and apply methods on functions

参考资料

this类型_TypeScript笔记11》上有1条评论

  1. Yu

    大佬的教程真的强,虽然我看过官方的文档,再看大佬的感觉自己没学过orz

    回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code