简介
类是面向对象编程(OOP)语言中常用的一种抽象,用来描述被称为对象的数据结构。这些对象可能包含一个初始状态并实现与该特定对象实例绑定的行为。2015年,ECMAScript 6为JavaScript引入了一种新的语法来创建类,在内部使用语言的原型功能。TypeScript完全支持这种语法,并在此基础上增加了一些功能,如成员可见性、抽象类、泛型类、箭头函数方法和其他一些功能。
本教程将介绍用于创建类的语法,可用的不同功能,以及TypeScript在编译时的类型检查中如何处理类。它将通过不同的代码样本来引导你,你可以在你自己的TypeScript环境中进行学习。
先决条件
- 一个环境,你可以在其中执行TypeScript程序,以跟随例子的发展。要在你的本地机器上设置这个,你需要以下东西。
- 同时安装Node和npm(或yarn),以便运行一个处理TypeScript相关包的开发环境。本教程在Node.js 14.3.0版本和npm 6.14.5版本中进行了测试。要在macOS或Ubuntu 18.04上安装,请按照《如何在macOS上安装Node.js并创建本地开发环境》中的步骤,或者按照《如何在Ubuntu 18.04上安装Node.js》中的《使用PPA安装》部分进行安装。如果你使用Windows Subsystem for Linux (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 类的_实例_,如下面的高亮代码所示。
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");
在突出显示的代码中,你向你的类的构造函数添加了一个类型为string ,名为name 的参数。然后,在创建一个新的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中,你通常要在类的主体中首先声明该属性,并给它一个类型。例如,给你的Person 类添加一个name 属性。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
在这个例子中,你除了在constructor 中设置属性外,还用类型string 声明了属性name 。
**注意:**在TypeScript中,你也可以声明类中属性的_可见性_,以确定数据可以被访问的位置。在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 参数的值。如果你给你的类添加更多的字段,这样写可能会变得很累。例如,在你的Person 类中添加一个类型为number ,名为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 。在设置参数属性时必须包含这个修饰符,而不会自动默认为public 。
如果你看一下TypeScript编译器发出的编译后的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 编译器会给你一个错误2420 。
OutputClass 'ConsoleLogger' incorrectly implements interface 'Logger'.
Property 'debug' is missing in type 'ConsoleLogger' but required in type 'Logger'. (2420)
如果你的实现与你正在实现的接口所期望的不一致,TypeScript编译器也会显示一个错误。例如,如果你把debug 方法中的message 参数的类型从string 改为number ,你会收到错误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 关键字,就像在突出显示的代码中一样。
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 方法将是现成的,不需要在你的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 类还必须明确地实现一个read 和一个write 方法,以遵守Stream 的抽象类。
如果你忘记了实现你所扩展的抽象类中的一个抽象成员,比如没有在你的FileStream 类中添加write 实现,TypeScript编译器会给出错误2515 。
OutputNon-abstract class 'FileStream' does not implement inherited abstract member 'write' from class 'Stream'. (2515)
如果你错误地实现了任何成员,比如将write 方法的第一个参数的类型改为string ,而不是Buffer ,TypeScript编译器也会显示一个错误。
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类语法,允许你指定类的成员的可见性。在这种情况下,_可见性指_的是实例化类之外的代码如何与类中的成员交互。
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 关键字,并在你的类中添加一个新的public 方法,名为getBirthYear ,它可以检索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 ,实例化一个具有Employee 形状的类。但如果你试图编译这段代码,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中的类更加强大,因为你可以访问类型系统,额外的语法,如箭头函数方法,以及全新的功能,如成员可见性和抽象类。这为你提供了一种方法,可以提供类型安全、更可靠的代码,并能更好地代表你的应用程序的商业模式。