一.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;
}
}
其中,addClass
与addClasses
的类型签名分别是:
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 toA<this: A>
. assignment compatibility flows normally like other generic type parameters.
具体的,this类型在实现上相当于A<this extends A<A>>
(即经典的CRTP 奇异递归模板模式),类中this
值的类型就是泛型参数this
。出了当前类/接口的上下文,this
的类型就是A<this: A>
,类型兼容性等与泛型一致
三.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类型,bind
、call
、apply
等场景也能正确维持类型约束,要求当前函数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.关于bind
、call
、apply
等类型约束的更多信息,见Strict bind, call, and apply methods on functions
大佬的教程真的强,虽然我看过官方的文档,再看大佬的感觉自己没学过orz