天天看点

如何在 TypeScript 中使用类

如何在 TypeScript 中使用类

英文 | https://www.digitalocean.com/community/tutorials/how-to-use-classes-in-typescript

翻译 | 杨小爱

介绍

类是面向对象编程 (OOP) 语言中用于描述对象的数据结构的常见对象。这些对象可能包含初始状态并实现绑定到该特定对象实例的行为。 

2015 年,ECMAScript 6 为 JavaScript 引入了一种新语法,以创建内部使用的原型语法类。TypeScript 完全支持该语法,并在其之上添加了一些特性,如可见性、抽象类、泛型类、箭头函数方法等。

本教程将介绍用于创建类的语法、可用的不同功能以及在编译时类型检查期间如何在 TypeScript 中处理类。它将引导我们完成具有不同代码示例的示例,我们可以在自己的 TypeScript 环境中遵循这些示例。

提前准备工作

要完成本教程,你需要做好以下工作:

一个环境,您可以在其中执行 TypeScript 程序以跟随示例。要在本地计算机上进行设置,您将需要准备以下内容:

为了运行处理 TypeScript 相关包的开发环境,同时,安装了 Node 和 npm(或 yarn)。本教程使用 Node.js 版本 14.3.0 和 npm 版本 6.14.5 进行了测试。要在 macOS 或 Ubuntu 18.04 上安装,请按照如何在 macOS 上安装 Node.js 和创建本地开发环境或如何在 Ubuntu 18.04 上安装 Node.js 的使用 PPA 安装部分中的步骤进行操作。如果您使用的是适用于 Linux 的 Windows 子系统 (WSL),这也适用。

此外,我们需要在机器上安装 TypeScript 编译器 (tsc)。为此,请参阅官方 TypeScript 网站,https://www.typescriptlang.org/download

如果你不想在本地机器上创建 TypeScript 环境,你可以使用官方的 TypeScript Playground 来完成。

您将需要足够的 JavaScript 知识,尤其是 ES6+ 语法,例如解构、rest 运算符和导入/导出。

本教程将参考支持 TypeScript 并显示内联错误的文本编辑器的各个方面。这不是使用 TypeScript 所必需的,但确实可以更多地利用 TypeScript 功能。为了获得这些好处,您可以使用像 Visual Studio Code 这样的文本编辑器,它完全支持开箱即用的 TypeScript。你也可以在 TypeScript Playground(地址:https://www.typescriptlang.org/play) 中尝试这些好处。

本教程中显示的所有示例都是使用 TypeScript 4.3.2 版创建的。

在 TypeScript 中创建类

在本节中,我们将学习用在 TypeScript 中创建类的语法示例。虽然,我们将介绍使用 TypeScript 创建类的一些基本方面,但语法与使用 JavaScript 创建类的语法基本相同。因此,本教程将重点介绍 TypeScript 中可用的一些显着特性。

我们可以使用 class 关键字创建类声明,后跟类名,然后是 {} 对块,如以下代码所示:​

class Person {
}      

该片段创建了一个名为 Person 的新类。然后,我们可以使用 new 关键字后跟的类的名称,接着是一个空参数列表(可以省略)来创建 Person 类的新实例,如以下突出显示的代码所示:

class Person {


}


const personInstance = new Person();      

我们可以将类本身视为创建具有给定形状的对象的蓝图,而实例是从该蓝图创建的对象本身。

在使用类时,大多数时候您需要创建一个constructor函数。constructor函数是每次创建类的新实例时运行的方法。这可用于初始化类中的值。

为Person 类引入一个constructor函数:​

class Person {
  constructor() {
    console.log("Constructor called");
  }
}


const personInstance = new Person();      

此constructor函数将在创建 personInstance 时将调用的构造函数记录到控制台。

constructor函数在接受参数的方式上类似于普通函数。当我们创建类的新实例时,这些参数将传递给构造函数。目前,我们没有将任何参数传递给constructor函数,如创建类的实例时的空参数列表 () 所示。

接下来,引入一个string(字符串)类型名称的新参数:​

class Person {
  constructor(name: string) {
    console.log(`Constructor called with name=${name}`);
  }
}


const personInstance = new Person("Jane");      

在突出显示的代码中,我们向类构造函数添加了一个名为字符串类型名称的参数。然后,在创建 Person 类的新实例时,我们还设置了该参数的值,在本例中“Jane”为字符串。最后,我们更改了 console.log 以将参数打印到屏幕上。

如果我们要运行此代码,我们将在终端中收到以下输出:

Output Constructor called with name=Jane      

构造函数中的参数在这里不是可选的。这意味着在实例化类时,必须将 name 参数传递给构造函数。如果不将 name 参数传递给构造函数,如下例所示:

const unknownPerson = new Person;      

TypeScript 编译器将报出错误 2554:

OutputExpected 1 arguments, but got 0. (2554)
filename.ts(4, 15): An argument for 'name' was not provided.      

现在我们已经在 TypeScript 中声明了一个类,我们将继续通过添加属性来操作这些类。

添加类属性

类最有用的地方之一是,它们能够保存从类创建的每个实例的内部数据。这是使用属性完成的。

TypeScript 有一些安全检查可以将此过程与 JavaScript 类区分开来,包括要求初始化属性以避免它们未定义。在本节中,我们将向你的类添加新属性以说明这些安全检查。

使用 TypeScript,我们通常必须首先在类的主体中声明属性并为其指定类型。例如,给 name 属性添加到 Person 类:

class Person {
  name: string;


  constructor(name: string) {
    this.name = name;
  }
}      

在此示例中,除了在构造函数中设置属性外,还使用类型字符串声明属性名称。

注意:在 TypeScript 中,我们还可以声明类中属性的可见性以确定可以访问数据的位置。在 name:string 声明中,没有声明可见性,这意味着该属性使用在任何地方都可以访问的默认公共状态。如果你想显示控制可见性,我们可以将其与属性一起声明。这将在本教程后面更深入地介绍。

我们还可以为属性提供默认值。例如,添加一个名为 instantiatedAt 的新属性,该属性将设置为实例化类实例的时间:

class Person {
  name: string;
  instantiatedAt = new Date();


  constructor(name: string) {
    this.name = name;
  }
}      

这使用 Date 对象来设置创建实例的初始日期。这段代码之所以有效,是因为在调用类构造函数时会执行默认值的代码,这相当于在构造函数上设置值,如下所示:

class Person {
  name: string;
  instantiatedAt: Date;


  constructor(name: string) {
    this.name = name;
    this.instantiatedAt = new Date();
  }
}      

通过在类的主体中声明默认值,我们无需在构造函数中设置该值。

注意,如果我们为类中的属性设置类型,则还必须将该属性初始化为该类型的值。为了说明这一点,声明一个类属性但不为其提供初始化器,如下面的代码所示:

class Person {
  name: string;
  instantiatedAt: Date;


  constructor(name: string) {
    this.name = name;
  }
}      

instantiatedAt 分配了一个 Date 类型,因此,必须始终是一个 Date 对象。但是由于没有初始化,所以当类被实例化时,属性变得未定义。因此,TypeScript 编译器将显示错误 2564:

Output Property 'instantiatedAt' has no initializer and is not definitely assigned in the constructor. (2564)      

这是一项额外的 TypeScript 安全检查,以确保在类实例化时存在正确的属性。

TypeScript 还有一个快捷方式,用于编写与传递给构造函数的参数同名的属性。此快捷方式称为参数属性。

在前面的示例中,我们将 name 属性设置为传递给类构造函数的 name 参数的值。如果我们在类中添加更多字段,这可能会变得令人厌烦。例如,在Person 类中添加一个名为 age 类型的新字段,并将其添加到构造函数中:

class Person {
  name: string;
  age: number;
  instantiatedAt = new Date();


  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}      

虽然这可行,但 TypeScript 可以使用参数属性或在构造函数的参数中设置的属性来减少此类样板代码:

class Person {
  instantiatedAt = new Date();


  constructor(
    public name: string,
    public age: number
) {}
}      

在此代码段中,我们从类主体中删除了 name 和 age 属性声明,并将它们移动到构造函数的参数列表中。当我们这样做时,我们是在告诉 TypeScript 这些构造函数参数也是该类的属性。这样,就不需要像以前那样将类的属性设置为构造函数中接收到的参数的值。

注意:可见性修饰符 public 已在代码中明确说明。设置参数属性时必须包含此修饰符,并且不会自动默认为公共可见性。

如果你查看由 TypeScript Compiler 发出的已编译 JavaScript,此代码将编译为以下 JavaScript 代码:

"use strict";
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    this.instantiatedAt = new Date();
  }
}      

这与原始示例编译成的 JavaScript 代码相同。

现在,我们已经尝试在 TypeScript 类上设置属性,可以继续将类扩展为具有类继承的新类。

TypeScript 中的类继承

TypeScript 提供了 JavaScript 类继承的全部功能,主要增加了两个功能:接口和抽象类。

接口是一种描述和强制类或对象形状的结构,例如为更复杂的数据片段提供类型检查。可以在类中实现接口以确保它具有特定的公共形状。抽象类是作为其他类的基础的类,但它们本身不能被实例化。这两者都是通过类继承实现的。

在本节中,我们将通过一些示例了解如何使用接口和抽象类来构建和创建类的类型检查。

实现接口

接口可用于指定该接口的所有实现必须具备的一组行为。接口是通过使用 interface 关键字后跟接口名称,然后是接口主体来创建的。例如,创建一个 Logger 接口,该接口可用于记录有关程序运行方式的重要数据:

interface Logger {}      

接下来,向您的界面添加四个方法:

interface Logger {
  debug(message: string, metadata?: Record<string, unknown>): void;
  info(message: string, metadata?: Record<string, unknown>): void;
  warning(message: string, metadata?: Record<string, unknown>): void;
  error(message: string, metadata?: Record<string, unknown>): void;
}      

如代码块所示,在接口中创建方法时,我们不会向它们添加任何实现,只添加它们的类型信息。

在这种情况下,有四种方法:调试、信息、警告和错误。它们都共享相同的类型签名:它们接收两个参数,一个字符串类型的消息和一个可选的 Record<string, unknown> 类型的元数据参数,它们都返回 void 类型。

实现此接口的所有类都必须为这些方法中的每一个具有相应的参数和返回类型。在名为 ConsoleLogger 的类中实现接口,该类使用控制台方法记录所有消息:

class ConsoleLogger implementsLogger {
  debug(message: string, metadata?: Record<string, unknown>) {
    console.info(`[DEBUG] ${message}`, metadata);
  }
  info(message: string, metadata?: Record<string, unknown>) {
    console.info(message, metadata);
  }
  warning(message: string, metadata?: Record<string, unknown>) {
    console.warn(message, metadata);
  }
  error(message: string, metadata?: Record<string, unknown>) {
    console.error(message, metadata);
  }
}      

注意,在创建接口时,我们使用了一个名为 implements 的新关键字来指定类实现的接口列表。

可以通过在 implements 关键字之后将它们添加为以逗号分隔的接口标识符列表来实现多个接口。例如,如果有另一个名为 Clearable 的接口:

interface Clearable {
  clear(): void;
}      

我们可以通过添加以下突出显示的代码在 ConsoleLogger 类中实现它:

class ConsoleLogger implements Logger, Clearable{
  clear() {
    console.clear();
  }
  debug(message: string, metadata?: Record<string, unknown>) {
    console.info(`[DEBUG] ${message}`, metadata);
  }
  info(message: string, metadata?: Record<string, unknown>) {
    console.info(message, metadata);
  }
  warning(message: string, metadata?: Record<string, unknown>) {
    console.warn(message, metadata);
  }
  error(message: string, metadata?: Record<string, unknown>) {
    console.error(message, metadata);
  }
}      

请注意,必须添加 clear 方法以确保该类遵循新接口。

如果没有为任何接口所需的成员之一提供实现,例如 Logger 接口中的调试方法,TypeScript 编译器会给您错误 2420:

Output Class 'ConsoleLogger' incorrectly implements interface 'Logger'.
  Property 'debug' is missing in type 'ConsoleLogger' but required in type 'Logger'. (2420)      

如果我们的实现与我们正在实现的接口所期望的不匹配,TypeScript 编译器也会显示错误。例如,如果将调试方法中的消息参数类型从字符串更改为数字,我们将收到错误 2416:

OutputProperty 'debug' in type 'ConsoleLogger' is not assignable to the same property in base type 'Logger'.
  Type '(message: number, metadata?: Record<string, unknown> | undefined) => void' is not assignable to type '(message: string, metadata: Record<string, unknown>) => void'.
    Types of parameters 'message' and 'message' are incompatible.
      Type 'string' is not assignable to type 'number'. (2416)      

建立在抽象类之上

抽象类与普通类类似,有两个主要区别:它们不能直接实例化,并且可能包含抽象元素。抽象元素是必须在继承类中实现的元素。它们在抽象类本身中没有实现。

这很有用,因为我们可以在基本抽象类中拥有一些通用功能,并在继承类中拥有更具体的实现。当我们将一个类标记为抽象时,我们是在说这个类缺少应该在继承类中实现的功能。

要创建抽象类,请在 class 关键字之前添加 abstract 关键字,如突出显示的代码:

abstractclass AbstractClassName {


}      

接下来,我们可以在抽象类中创建对象,其中一些可能有实现,而另一些则没有。没有实现的被标记为抽象,然后必须在从抽象类扩展的类中实现。

例如,假设我们在 Node.js 环境中工作,并且正在创建自己的 Stream 实现。为此,我们将拥有一个名为 Stream 的抽象类,它有两个抽象类,read和write:

declare class Buffer {
  from(array: any[]): Buffer;
  copy(target: Buffer, offset?: number): void;
}


abstract class Stream {


  abstract read(count: number): Buffer;


  abstract write(data: Buffer): void;
}      

这里的 Buffer 对象是 Node.js 中可用的一个类,用于存储二进制数据。顶部的 declare class Buffer 语句允许代码在没有 Node.js 类型声明的 TypeScript 环境中编译,例如 TypeScript Playground。

在这个例子中,read 方法从内部数据结构中计算字节数并返回一个 Buffer 对象,并将实例的write所有内容写入Buffer流中。这两种方法都是抽象的,只能在从 Stream 扩展的类中实现。

然后,我们可以创建具有实现的其他方法。这样,从Stream 抽象类扩展的任何类都将自动接收这些方法。copy方法实例如下:

declare class Buffer {
  from(array: any[]): Buffer;
  copy(target: Buffer, offset?: number): void;
}


abstract class Stream {


  abstract read(count: number): Buffer;


  abstract write(data: Buffer): void;


  copy(count: number, targetBuffer: Buffer, targetBufferOffset: number) {
    const data = this.read(count);
    data.copy(targetBuffer, targetBufferOffset);
  }
}      

copy方法将从流中读取字节的结果复制到 targetBuffer,从 targetBufferOffset 开始。

如果我们为 Stream 抽象类创建一个实现,如 FileStream 类,则复制方法将很容易使用,而无需在 FileStream 类中复制它:

declare class Buffer {
  from(array: any[]): Buffer;
  copy(target: Buffer, offset?: number): void;
}


abstract class Stream {


  abstract read(count: number): Buffer;


  abstract write(data: Buffer): void;


  copy(count: number, targetBuffer: Buffer, targetBufferOffset: number) {
    const data = this.read(count);
    data.copy(targetBuffer, targetBufferOffset);
  }
}


class FileStream extends Stream {
  read(count: number): Buffer {
    // implementation here
    return new Buffer();
  }


  write(data: Buffer) {
    // implementation here
  }
}


const fileStream = new FileStream();      

在此示例中,fileStream 实例自动具有可用的copy方法。FileStream 类还必须显示实现读取和写入方法以遵守 Stream 抽象类。

如果我们忘记要从中扩展的抽象类中抽象某元素,例如,未在 FileStream 类中添加write,TypeScript 编译器将给出错误 2515:

OutputNon-abstract class 'FileStream' does not implement inherited abstract member 'write' from class 'Stream'. (2515)      

如果我们错误地实现了任何元素,TypeScript 编译器也会显示错误,例如,将 write 方法的第一个参数的类型更改为 string 类型而不是 Buffer:

OutputProperty 'write' in type 'FileStream' is not assignable to the same property in base type 'Stream'.
  Type '(data: string) => void' is not assignable to type '(data: Buffer) => void'.
    Types of parameters 'data' and 'data' are incompatible.
      Type 'Buffer' is not assignable to type 'string'. (2416)      

使用抽象类和接口,我们可以对类5+进行更复杂的类型检查,以确保从基类扩展的类继承正确的功能。接下来,您将通过示例了解 TypeScript 中的方法和属性可见性如何工作。

类成员可见性

TypeScript 通过允许指定类成员的可见性来增强可用的 JavaScript 类语法。在这种情况下,可见性是指实例化类之外的代码如何与类内的成员交互。

TypeScript 中的类成员可能具有三种可能的可见性修饰符:public、protected 和 private。

公共成员可以在类实例之外访问,而私有成员则不能。protected 介于两者之间,其中成员可以由类的实例或基于该类的子类访问。

在本节中,我们将检查可用的可见性修饰符并了解它们的含义。

public

这是 TypeScript 中类成员的默认可见性。当我们不将可见性修饰符添加到类成员时,它与将其设置为 public 相同。公共类成员可以在任何地方访问,没有任何限制。

为了说明这一点,回到前面的 Person 类:

class Person {
  public instantiatedAt = new Date();


  constructor(
    name: string,
    age: number
) {}
}      

本教程提到,默认情况下,name和age这两个属性具有公开可见性。要显示声明类型可见性,请在属性之前添加 public 关键字,并为类添加一个名为 getBirthYear 的新公共方法,该方法检索 Person 实例的出生年份:

class Person {
  constructor(
    publicname: string,
    publicage: number
) {}


  public getBirthYear() {
    return new Date().getFullYear() - this.age;
  }
}      

然后,我们可以在类实例之外的全局空间中使用属性和方法:

class Person {
  constructor(
    public name: string,
    public age: number
) {}


  public getBirthYear() {
    return new Date().getFullYear() - this.age;
  }
}


const jon = new Person("Jon", 35);


console.log(jon.name);
console.log(jon.age);
console.log(jon.getBirthYear());      

此代码会将以下内容打印到控制台:

OutputJon
35
1986      

请注意,我们可以访问班级的所有成员。

protected

具有受保护可见性的类成员只允许在声明它们的类内部或该类的子类中使用。

看看下面的 Employee 类和基于它的 FinanceEmployee 类:

class Employee {
  constructor(
    protected identifier: string
  ) {}
}


class FinanceEmployee extends Employee {
  getFinanceIdentifier() {
    return `fin-${this.identifier}`;
  }
}      

突出显示的代码显示了声明为受保护可见性的标识符属性。this.identifier 代码尝试从 FinanceEmployee 子类访问此属性。此代码将在 TypeScript 中运行而不会出错。

如果尝试从不在类本身或子类内部的位置使用该方法,如下例所示:

class Employee {
  constructor(
    protected identifier: string
  ) {}
}


class FinanceEmployee extends Employee {
  getFinanceIdentifier() {
    return `fin-${this.identifier}`;
  }
}


const financeEmployee = new FinanceEmployee('abc-12345');
financeEmployee.identifier;      

TypeScript 编译器会给我们报出错误,2445:

OutputProperty 'identifier' is protected and only accessible within class 'Employee' and its subclasses. (2445)      

这是因为无法从全局空间中检索到新的 financeEmployee 实例的标识符属性。相反,必须使用内部方法 getFinanceIdentifier 来返回包含标识符属性的字符串:

class Employee {
  constructor(
    protected identifier: string
  ) {}
}


class FinanceEmployee extends Employee {
  getFinanceIdentifier() {
    return `fin-${this.identifier}`;
  }
}


const financeEmployee = new FinanceEmployee('abc-12345');
console.log(financeEmployee.getFinanceIdentifier())      

这会将以下内容记录到控制台:

Output fin-abc-12345      

private

私有成员只能在声明它们的类内部访问。这意味着即使是子类也无法访问它。

使用前面的示例,将 Employee 类中的标识符属性转换为私有属性:​

class Employee {
  constructor(
    privateidentifier: string
  ) {}
}


class FinanceEmployee extends Employee {
  getFinanceIdentifier() {
    return `fin-${this.identifier}`;
  }
}      

此代码现在将导致 TypeScript 编译器显示错误 2341:​

Output 
Property 'identifier' is private and only accessible within class 'Employee'. (2341)      

发生这种情况是因为我们正在访问 FinanceEmployee 子类中的属性标识符,这是不允许的,因为标识符属性是在 Employee 类中声明的,并且其可见性设置为私有。

请记住,TypeScript 被编译为原始 JavaScript,其本身无法指定类成员的可见性。因此,TypeScript 在运行时没有针对此类使用的保护。这是 TypeScript 编译器仅在编译期间完成的安全检查。

现在,已经尝试了可见性修饰符,我们可以继续使用箭头函数作为 TypeScript 类中的方法。

类方法作为箭头函数

在 JavaScript 中,表示函数上下文的 this 值可以根据函数的调用方式而改变。这种可变性有时会在复杂的代码中造成混淆。使用 TypeScript 时,可以在创建类方法时使用特殊语法,以避免将其绑定到类实例以外的其他东西。在本节中,我们将尝试这种语法。

使用 Employee 类,引入一个仅用于检索员工标识符的新方法:​

class Employee {
  constructor(
    protected identifier: string
) {}


  getIdentifier() {
  return this.identifier;
  }
}      

如果直接调用该方法,这将非常有效:

class Employee {
  constructor(
    protected identifier: string
) {}


  getIdentifier() {
    return this.identifier;
  }
}


const employee = new Employee("abc-123");


console.log(employee.getIdentifier());      

这会将以下内容打印到控制台的输出:​

Output abc-123      

但是,如果将 getIdentifier 实例方法存储在某处以供稍后调用,如以下代码所示:

class Employee {
  constructor(
    protected identifier: string
) {}


  getIdentifier() {
    return this.identifier;
  }
}


const employee = new Employee("abc-123");


const obj = {
  getId: employee.getIdentifier
}


console.log(obj.getId());      

该值将无法访问:​

Output undefined      

发生这种情况是因为当我们调用 obj.getId() 时,employee.getIdentifier 内部的 this 现在绑定到 obj 对象,而不是 Employee 实例。

可以通过将 getIdentifier 更改为箭头函数来避免这种情况。检查以下代码中突出显示的更改:

class Employee {
  constructor(
    protected identifier: string
) {}


  getIdentifier = () => {
    return this.identifier;
  }
}
...      

如果现在尝试像以前一样调用 obj.getId(),控制台会正确显示:​

abc-123      

这演示了 TypeScript 如何允许箭头函数用作类方法的直接值。

在下一节中,将学习如何使用 TypeScript 的类型检查来强制类。

使用类作为类型

到目前为止,本教程已经介绍了如何创建类并直接使用它们。

在本节中,我们将在使用 TypeScript 时将类用作类型。

类在 TypeScript 中既是类型又是值,因此,可两种方式使用。要将类用作类型,请在 TypeScript 需要类型的任何地方使用类名。例如,给定之前创建的 Employee 类:​

class Employee {
  constructor(
    public identifier: string
) {}
}      

想象一下,我们想创建一个打印任何员工标识符的函数。我们可以像这样创建这样的函数:​

class Employee {
  constructor(
    public identifier: string
) {}
}


function printEmployeeIdentifier(employee: Employee) {
  console.log(employee.identifier);
}      

请注意,将employee 参数设置为Employee 类型,这是类的确切名称。

TypeScript 中的类与其他类型(包括其他类)进行比较,就像在 TypeScript 中比较其他类型一样:在结构上。这意味着,如果有两个具有相同形状的不同类(即具有相同可见性的同一组成员),则它们可以在只期望其中一个的地方互换使用。

为了说明这一点,假设我们的应用程序中有另一个名为 Warehouse 的类:​

class Warehouse {
  constructor(
    public identifier: string
) {}
}      

它的形状与 Employee 相同。如果尝试将其实例传递给 printEmployeeIdentifier:

class Employee {
  constructor(
    public identifier: string
) {}
}


class Warehouse {
  constructor(
    public identifier: string
) {}
}


function printEmployeeIdentifier(employee: Employee) {
  console.log(employee.identifier);
}


const warehouse = new Warehouse("abc");


printEmployeeIdentifier(warehouse);      

TypeScript 编译器不会抱怨。甚至可以只使用普通对象而不是类的实例。由于这可能会导致刚开始使用 TypeScript 的程序员所不期望的行为,因此密切关注这些情况非常重要。

通过使用类作为类型的基础知识,现在可以学习如何检查特定类,而不仅仅是形状。

this类型

有时,我们需要在类本身的某些方法中引用当前类的类型。

在本节中,我们将了解如何使用它来完成此操作。

想象一下,必须向 Employee 类添加一个名为 isSameEmployeeAs 的新方法,该方法将负责检查另一个员工实例是否引用与当前员工相同的员工。可以这样做,方法如下:

class Employee {
  constructor(
    protected identifier: string
  ) {}


  getIdentifier() {
    return this.identifier;
  }


  isSameEmployeeAs(employee: Employee) {
    return this.identifier === employee.identifier;
  }
}      

该测试将用于比较从 Employee 派生的所有类的标识符属性。但是想象一个场景,根本不希望比较 Employee 的特定子类。在这种情况下,希望 TypeScript 在比较两个不同的子类时报告错误,而不是接收比较的布尔值。

例如,为财务和营销部门的员工创建两个新的子类:

...
class FinanceEmployee extends Employee {
  specialFieldToFinanceEmployee = '';
}


class MarketingEmployee extends Employee {
  specialFieldToMarketingEmployee = '';
}


const finance = new FinanceEmployee("fin-123");
const marketing = new MarketingEmployee("mkt-123");


marketing.isSameEmployeeAs(finance);      

在这里,从 Employee 基类派生了两个类:FinanceEmployee 和 MarketingEmployee。每个都有不同的新领域。

然后,将为每个实例创建一个实例,并检查营销员工是否与财务员工相同。鉴于这种情况,TypeScript 应该报告错误,因为根本不应该比较子类。

这不会发生,因为在 isSameEmployeeAs 方法中使用 Employee 作为员工参数的类型,并且从 Employee 派生的所有类都将通过类型检查。

要改进此代码,可以使用类内部可用的特殊类型,即 this 类型。该类型被动态设置为当前类的类型。这样,当在派生类中调用此方法时,会将 this 设置为派生类的类型。

更改的代码以使用this:

class Employee {
  constructor(
    protected identifier: string
  ) {}


  getIdentifier() {
    return this.identifier;
  }


  isSameEmployeeAs(employee: this) {
    return this.identifier === employee.identifier;
  }
}


class FinanceEmployee extends Employee {
  specialFieldToFinanceEmployee = '';
}


class MarketingEmployee extends Employee {
  specialFieldToMarketingEmployee = '';
}


const finance = new FinanceEmployee("fin-123");
const marketing = new MarketingEmployee("mkt-123");


marketing.isSameEmployeeAs(finance);      

编译此代码时,TypeScript 编译器现在将显示错误 2345:

Output Argument of type 'FinanceEmployee' is not assignable to parameter of type 'MarketingEmployee'.
  Property 'specialFieldToMarketingEmployee' is missing in type 'FinanceEmployee' but required in type 'MarketingEmployee'. (2345)      

使用 this 关键字,可以在不同的类上下文中动态更改类型。接下来,将使用类型来传递类本身,而不是类的实例。

使用构造签名

有时程序员需要创建一个直接采用类而不是实例的函数。为此,需要使用带有构造签名的特殊类型。

在本节中,将了解如何创建此类类型。

可能需要传入类本身的一个特定场景是类工厂,或生成作为参数传入的类的新实例的函数。想象一下,创建一个函数,该函数接受一个基于 Employee 的类,创建一个具有递增标识符的新实例,并将标识符打印到控制台。可以尝试如下创建:

class Employee {
  constructor(
    public identifier: string
) {}
}


let identifier = 0;
function createEmployee(ctor: Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}      

在此代码段中,将创建 Employee 类,初始化标识符,并创建一个函数,该函数基于具有 Employee 形状的构造函数参数 ctor 实例化一个类。但是如果试图编译这段代码,TypeScript 编译器会给出错误 2351:

Output This expression is not constructable.
  Type 'Employee' has no construct signatures. (2351)      

发生这种情况是因为,当使用类的名称作为 ctor 的类型时,该类型仅对类的实例有效。要获取类构造函数本身的类型,必须使用 typeof ClassName。检查以下突出显示的代码进行更改:

class Employee {
  constructor(
    public identifier: string
) {}
}


let identifier = 0;
function createEmployee(ctor: typeof Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}      

现在的代码将成功编译。但是还有一个悬而未决的问题:由于类工厂构建了从基类构建的新类的实例,因此,使用抽象类可以改进工作流程。但是,这最初不起作用。

要尝试这一点,请将 Employee 类转换为abstract(抽象)类:

abstractclass Employee {
  constructor(
    public identifier: string
) {}
}


let identifier = 0;
function createEmployee(ctor: typeof Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}      

TypeScript 编译器现在将给出错误,2511:

Output Cannot create an instance of an abstract class. (2511)      

此错误表明无法从 Employee 类创建实例,因为它是抽象的。但是,可能希望使用这样的函数来创建从 Employee 抽象类扩展而来的不同类型的员工,例如:

abstract class Employee {
  constructor(
    public identifier: string
  ) {}
}


class FinanceEmployee extends Employee {}


class MarketingEmployee extends Employee {}


let identifier = 0;
function createEmployee(ctor: typeof Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}


createEmployee(FinanceEmployee);
createEmployee(MarketingEmployee);      

要使代码适用于这种情况,必须使用具有构造函数签名的类型。可以使用 new 关键字,后跟类似于箭头函数的语法来执行此操作,其中参数列表包含构造函数预期的参数,返回类型是此构造函数返回的类实例。

以下代码中突出显示的更改是将具有构造函数签名的类型引入 createEmployee 函数:

abstract class Employee {
  constructor(
    public identifier: string
) {}
}


class FinanceEmployee extends Employee {}


class MarketingEmployee extends Employee {}


let identifier = 0;
function createEmployee(ctor: new (identifier: string) => Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}


createEmployee(FinanceEmployee);
createEmployee(MarketingEmployee);      

TypeScript 编译器现在将正确编译代码。

结论

TypeScript 中的类比 JavaScript 中的类更强大,因为可以访问类型系统、箭头函数方法等额外语法以及成员可见性和抽象类等全新功能。这为我们提供了一种交付类型安全、更可靠且更好地代表应用程序业务模型的代码的方法。

学习更多技能

请点击下方公众号

继续阅读