如何在TypeScript中使用类
作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。
介绍
类是 面向对象编程 (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 网站。
- 如果您不想在本地机器上创建 TypeScript 环境,您可以使用官方的 TypeScript Playground 进行操作。
- 您将需要足够的 JavaScript 知识,尤其是 ES6+ 语法,例如 解构、剩余运算符 和 导入/导出 。 如果您需要有关这些主题的更多信息,建议阅读我们的 如何在 JavaScript 中编码系列。
- 本教程将参考支持 TypeScript 并显示内联错误的文本编辑器的各个方面。 这不是使用 TypeScript 所必需的,但确实可以更多地利用 TypeScript 功能。 为了获得这些好处,您可以使用像 Visual Studio Code 这样的文本编辑器,它完全支持开箱即用的 TypeScript。 您还可以在 TypeScript Playground 中尝试这些好处。
本教程中显示的所有示例都是使用 TypeScript 4.3.2 版创建的。
在 TypeScript 中创建类
在本节中,您将浏览用于在 TypeScript 中创建类的语法示例。 虽然您将介绍使用 TypeScript 创建类的一些基本方面,但语法与 使用 JavaScript 创建类的语法基本相同。 因此,本教程将重点介绍 TypeScript 中可用的一些显着特性。
您可以使用 class
关键字创建类声明,后跟类名,然后是 {}
对块,如以下代码所示:
class Person { }
这个片段创建了一个名为 Person
的新类。 然后,您可以使用 new
关键字后跟您的类的名称,然后是一个空参数列表(可以省略)来创建 Person
类的新 instance ),如以下突出显示的代码所示:
class Person { } const personInstance = new Person();
您可以将类本身视为创建具有给定形状的对象的蓝图,而实例是从该蓝图创建的对象本身。
使用类时,大多数时候您需要创建一个 constructor
函数。 constructor
是每次创建类的新实例时运行的方法。 这可用于初始化类中的值。
为您的 Person
类引入一个构造函数:
class Person { constructor() { console.log("Constructor called"); } } const personInstance = new Person();
当创建 personInstance
时,此构造函数会将 Constructor called
记录到控制台。
构造函数在接受参数的方式上类似于普通函数。 当您创建类的新实例时,这些参数将传递给构造函数。 目前,您没有将任何参数传递给构造函数,如创建类实例时的空参数列表 ()
所示。
接下来,引入一个名为 name
的新参数,类型为 string
:
class Person { constructor(name: string) { console.log(`Constructor called with name=${name}`); } } const personInstance = new Person("Jane");
在突出显示的代码中,您向类构造函数添加了一个名为 name
、类型为 string
的参数。 然后,在创建 Person
类的新实例时,您还设置了该参数的值,在本例中为字符串 "Jane"
。 最后,您更改了 console.log
以将参数打印到屏幕上。
如果您要运行此代码,您将在终端中收到以下输出:
OutputConstructor 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 类区分开来,包括要求初始化属性以避免它们成为 undefined
。 在本节中,您将向您的类添加新属性以说明这些安全检查。
使用 TypeScript,您通常必须首先在类的主体中声明属性并为其指定类型。 例如,将 name
属性添加到 Person
类:
class Person { name: string; constructor(name: string) { this.name = name; } }
在此示例中,除了在 constructor
中设置属性外,还使用类型 string
声明属性 name
。
注意: 在 TypeScript 中,你也可以在一个类中声明属性的 visibility 来决定数据可以被访问到哪里。 在 name: string
声明中,没有声明可见性,这意味着该属性使用在任何地方都可以访问的默认 public
状态。 如果您想显式控制可见性,您可以将其与属性一起声明。 这将在本教程后面更深入地介绍。
您还可以为属性提供默认值。 例如,添加一个名为 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
对象。 但是由于没有初始化,所以类实例化时属性变为undefined
。 因此,TypeScript 编译器将显示错误 2564
:
OutputProperty 'instantiatedAt' has no initializer and is not definitely assigned in the constructor. (2564)
这是一项额外的 TypeScript 安全检查,以确保在类实例化时存在正确的属性。
TypeScript 还有一个快捷方式,用于编写与传递给构造函数的参数同名的属性。 此快捷方式称为 参数属性 。
在前面的示例中,您将 name
属性设置为传递给类构造函数的 name
参数的值。 如果您在类中添加更多字段,这可能会变得令人厌烦。 例如,将类型为 number
的名为 age
的新字段添加到您的 Person
类,并将其添加到构造函数中:
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
已在代码中明确说明。 设置参数属性时必须包含此修饰符,并且不会自动默认为 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; }
如此代码块所示,在您的接口中创建方法时,您不会向它们添加任何实现,只添加它们的类型信息。 在这种情况下,您有四种方法:debug
、info
、warning
和 error
。 它们都共享相同的类型签名:它们接收两个参数,一个 string
类型的 message
和一个 Record<string, unknown>
类型的可选 metadata
参数。 它们都返回 void
类型。
实现此接口的所有类都必须为这些方法中的每一个具有相应的参数和返回类型。 在名为 ConsoleLogger
的类中实现接口,该类使用 console
方法记录所有消息:
class ConsoleLogger implements Logger { 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
接口中的 debug
方法,TypeScript 编译器会给您错误 [ X209X]:
OutputClass 'ConsoleLogger' incorrectly implements interface 'Logger'. Property 'debug' is missing in type 'ConsoleLogger' but required in type 'Logger'. (2420)
如果您的实现与您正在实现的接口所期望的不匹配,TypeScript 编译器也会显示错误。 例如,如果您将 debug
方法中的 message
参数类型从 string
更改为 number
,则会收到错误 [X145X ]:
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
关键字,如突出显示的代码:
abstract class AbstractClassName { }
接下来,您可以在抽象类中创建成员,其中一些可能有实现,而另一些则没有。 没有实现的标记为 abstract
,然后必须在从抽象类扩展的类中实现。
例如,假设您在 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
类,则 copy
方法将很容易使用,而无需在 中复制它【X188X】类:
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
类还必须显式实现 read
和 write
方法以遵守 Stream
抽象类。
如果您忘记实现要扩展的抽象类的抽象成员之一,例如未在 FileStream
类中添加 write
实现,TypeScript 编译器会给出错误 2515
:
OutputNon-abstract class 'FileStream' does not implement inherited abstract member 'write' from class 'Stream'. (2515)
如果您错误地实现了任何成员,TypeScript 编译器也会显示错误,例如将 write
方法的第一个参数的类型更改为 string
而不是 [ X212X]:
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)
使用抽象类和接口,您可以对您的类进行更复杂的类型检查,以确保从基类扩展的类继承正确的功能。 接下来,您将通过示例了解 TypeScript 中的方法和属性可见性如何工作。
类成员可见性
TypeScript 通过允许您指定类成员的可见性来增强可用的 JavaScript 类语法。 在这种情况下,visibility 指的是实例化类之外的代码如何与类内的成员交互。
TypeScript 中的类成员可能具有三种可能的可见性修饰符:public
、protected
和 private
。 public
成员可以在类实例之外访问,而 private
则不能。 protected
介于两者之间,其中成员可以由类的实例或基于该类的子类访问。
在本节中,您将检查可用的可见性修饰符并了解它们的含义。
public
这是 TypeScript 中类成员的默认可见性。 不给类成员添加可见性修饰符时,与将其设置为 public
相同。 公共类成员可以在任何地方访问,没有任何限制。
为了说明这一点,回到前面的 Person
类:
class Person { public instantiatedAt = new Date(); constructor( name: string, age: number ) {} }
本教程提到 name
和 age
这两个属性默认具有 public
可见性。 要显式声明类型可见性,请在属性之前添加 public
关键字,并在名为 getBirthYear
的类中添加新的 public
方法,该方法检索 Person
实例:
class Person { constructor( public name: string, public age: 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
具有 protected
可见性的类成员只允许在声明它们的类内部或该类的子类中使用。
看看下面的 Employee
类和基于它的 FinanceEmployee
类:
class Employee { constructor( protected identifier: string ) {} } class FinanceEmployee extends Employee { getFinanceIdentifier() { return `fin-${this.identifier}`; } }
突出显示的代码显示了使用 protected
可见性声明的 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
实例的 identifier
属性。 相反,您必须使用内部方法 getFinanceIdentifier
来返回包含 identifier
属性的字符串:
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())
这会将以下内容记录到控制台:
Outputfin-abc-12345
private
私有成员只能在声明它们的类内部访问。 这意味着即使是子类也无法访问它。
使用前面的示例,将 Employee
类中的 identifier
属性转换为 private
属性:
class Employee { constructor( private identifier: string ) {} } class FinanceEmployee extends Employee { getFinanceIdentifier() { return `fin-${this.identifier}`; } }
此代码现在将导致 TypeScript 编译器显示错误 2341
:
OutputProperty 'identifier' is private and only accessible within class 'Employee'. (2341)
发生这种情况是因为您正在访问 FinanceEmployee
子类中的属性 identifier
,这是不允许的,因为 identifier
属性是在 Employee
类中声明的并将其可见性设置为 private
。
请记住,TypeScript 被编译为原始 JavaScript,它本身无法指定类成员的可见性。 因此,TypeScript 在运行时没有针对此类使用的保护。 这是 TypeScript 编译器仅在编译期间完成的安全检查。
现在您已经尝试了可见性修饰符,您可以继续使用箭头函数作为 TypeScript 类中的方法。
类方法作为箭头函数
在 JavaScript 中,表示函数上下文的 this 值可以根据调用函数的方式而改变。 这种可变性有时会在复杂的代码中造成混淆。 使用 TypeScript 时,您可以在创建类方法时使用特殊语法,以避免 this
绑定到类实例以外的其他东西。 在本节中,您将尝试这种语法。
使用您的 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());
这会将以下内容打印到控制台的输出:
Outputabc-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());
该值将无法访问:
Outputundefined
发生这种情况是因为当您调用 obj.getId()
时,employee.getIdentifier
内部的 this
现在绑定到 obj
对象,而不是 Employee
实例。
您可以通过将 getIdentifier
更改为 箭头函数 来避免这种情况。 检查以下代码中突出显示的更改:
class Employee { constructor( protected identifier: string ) {} getIdentifier = () => { return this.identifier; } } ...
如果您现在尝试像以前一样调用 obj.getId()
,控制台会正确显示:
Outputabc-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
的类型
有时您需要在类本身的某些方法中引用当前类的类型。 在本节中,您将了解如何使用 this
来完成此操作。
想象一下,您必须向您的 Employee
类添加一个名为 isSameEmployeeAs
的新方法,该方法将负责检查另一个员工实例是否引用与当前员工相同的员工。 您可以这样做的一种方法如下:
class Employee { constructor( protected identifier: string ) {} getIdentifier() { return this.identifier; } isSameEmployeeAs(employee: Employee) { return this.identifier === employee.identifier; } }
此测试将用于比较从 Employee
派生的所有类的 identifier
属性。 但是想象一个场景,您根本不希望比较 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
。 每个都有不同的新领域。 然后,您将为每个实例创建一个实例,并检查 marketing
员工是否与 finance
员工相同。 鉴于这种情况,TypeScript 应该报告错误,因为根本不应该比较子类。 这不会发生,因为您在 isSameEmployeeAs
方法中使用 Employee
作为 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
:
OutputArgument 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
类,初始化 identifier
,并创建了一个函数,该函数基于构造函数参数 ctor
实例化一个类,其形状为 [X188X ]。 但是如果你试图编译这段代码,TypeScript 编译器会给出错误 2351
:
OutputThis 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); }
现在您的代码将成功编译。 但是还有一个悬而未决的问题:由于类工厂构建从基类构建的新类的实例,使用 abstract
类可以改进工作流程。 但是,这最初不起作用。
要尝试这一点,请将 Employee
类转换为 abstract
类:
abstract class 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
:
OutputCannot create an instance of an abstract class. (2511)
此错误表明您无法从 Employee
类创建实例,因为它是 abstract
。 但是您可能希望使用这样的函数来创建从 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 中的类更强大,因为您可以访问类型系统、箭头函数方法等额外语法以及成员可见性和抽象类等全新功能。 这为您提供了一种交付类型安全、更可靠且更好地代表应用程序业务模型的代码的方法。
有关 TypeScript 的更多教程,请查看我们的 How To Code in TypeScript 系列页面。