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

参考资料

  • Supporting ‘this’ type
  • Polymorphic ‘this’ type
  • TypeScript Design Notes for 9/18/2015
  • Function this types
  • This function types
  • Bounded quantification
  • Curiously recurring template pattern
  • CSE 505 — Very Rough Notes on F-Bounded Polymorphism

本文分享自微信公众号 - 前端向后(backward-fe)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-03-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏深度学习思考者

CNN卷积神经网络的改进(15年最新paper)

回归正题,今天要跟大家分享的是一些 Convolutional Neural Networks(CNN)的工作。大家都知道,CNN 最早提出时,是以一定的人眼生...

39250
来自专栏素质云笔记

keras系列︱利用fit_generator最小化显存占用比率/数据Batch化

运行机器学习算法时,很多人一开始都会有意无意将数据集默认直接装进显卡显存中,如果处理大型数据集(例如图片尺寸很大)或是网络很深且隐藏层很宽,也可能造成显存不足。

16330
来自专栏机器学习入门

算法细节系列(6):410. Split Array Largest Sum

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.n...

12130
来自专栏素质云笔记

neo4j︱Cypher 查询语言简单案例(二)

版权声明:博主原创文章,微信公众号:素质云笔记,转载请注明来源“素质云博客”,谢谢合作!! ...

13230
来自专栏机器学习入门

LeetCode Weekly Contest 23 之 536.Construct Binary Tree from String

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.n...

13040
来自专栏运维前线

CentOS 7.2 部署Node.js开发环境

版权声明:本文为木偶人shaon原创文章,转载请注明原文地址,非常感谢。 https://b...

24820
来自专栏机器学习入门

算法细节系列(34):再见字符串(2)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.n...

5630
来自专栏更流畅、简洁的软件开发方式

基于nodejs的流水线式的CRUD服务。依赖注入可以支持插件。

当我们刚开始学习数据库编程的时候,我们会先写一段代码,实现往一个表里添加数据的功能。这段代码是必须写的,不写怎么会?

11720
来自专栏运维前线

如何在Linux上安装Node.js

版权声明:本文为木偶人shaon原创文章,转载请注明原文地址,非常感谢。 https://b...

20720
来自专栏机器学习入门

LeetCode Weekly Contest 36解题思路

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.n...

7530

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励