TypeScript中接口和类
1. 接口
类似于对象类型字面量,接口类型也能够表示任意的对象类型。不同的是,接口类型能够给对象类型命名以及定义类型参数。接口类型无法表示原始类型,如boolean类型等。
接口声明只存在于编译阶段,在编译后生成的JavaScript代码中不包含任何接口代码。
1.1 接口声明
通过接口声明能够定义一个接口类型。接口声明的基础语法如下所示:
interface InterfaceName
{
TypeMember;
TypeMember;
...
}
在该语法中,interface是关键字,InterfaceName表示接口名,它必须是合法的标识符,TypeMember表示接口的类型成员,所有类型成员都置于一对大括号“{}”之内。
按照惯例,接口名的首字母需要大写。因为接口定义了一种类型,而类型名的首字母通常需要大写。示例如下:
interface Shape { }
在接口名之后,由一对大括号“{}”包围起来的是接口类型中的类型成员。这部分的语法对象类型字面量的语法完全相同。从语法的角度来看,接口声明就是在对象类型字面量之前添加了interface关键字和接口名。因此,对象类型字面量的语法规则同样适用于接口声明。
接口类型的类型成员(TypeMember)分为以下五类:
- 属性签名
- 调用签名
- 构造签名
- 方法签名
- 索引签名
1.2 属性签名
属性签名声明了对象类型中属性成员的名称和类型。属性签名的语法如下所示:
PropertyName: Type;
在该语法中,PropertyName表示对象属性名,可以为标识符、字符串、数字和可计算属性名;Type表示该属性的类型。eg:
interface Point {
x: number;
y: number;
}
1.3 调用签名
调用签名定义了该对象类型表示的函数在调用时的类型参数、参数列表以及返回值类型。调用签名的语法如下所示:
(ParameterList): Type
在该语法中,ParameterList表示函数形式参数列表类型;Type表示函数返回值类型,两者都是可选的。eg:
interface ErrorConstructor {
(message?: string): Error;
}
1.4 构造签名
构造签名定义了该对象类型表示的构造函数在使用new运算符调用时的参数列表以及返回值类型。eg:
new (ParameterList): Type
在该语法中,new是运算符关键字;ParameterList表示构造函数形式参数列表类型;Type表示构造函数返回值类型,两者都是可选的。eg:
interface ErrorConstructor {
new (message?: string): Error;
}
1.5 方法签名
方法签名是声明函数类型的属性成员的简写。eg:
PropertyName(ParameterList): Type
在该语法中,PropertyName表示对象属性名,可以为标识符、字符串、数字和可计算属性名;ParameterList表示可选的方法形式参数列表类型;Type表示可选的方法返回值类型。从语法的角度来看,方法签名是在调用签名之前添加一个属性名作为方法名。eg :
interface Document {
getElementById(elementId: string): HTMLElement | null;
}
1.6 索引签名
JavaScript支持使用索引去访问对象的属性,即通过方括号“[]”语法去访问对象属性。一个典型的例子是数组对象,我们既可以使用数字索引去访问数组元素,也可以使用字符串索引去访问数组对象上的属性和方法。示例如下:
const colors = ['red', 'green', 'blue'];
// 访问数组中的第一个元素
const red = colors[0];
// 访问数组对象的length属性
const len = colors['length'];
接口中的索引签名能够描述使用索引访问的对象属性的类型。索引签名只有以下两种:
- 字符串索引签名。
//字符串索引签名的语法
[IndexName: string]: Type
//eg
interface A {
[prop: string]: number;
}
- 数值索引签名。
//数值索引签名 语法
[IndexName: number]: Type
//eg :
interface A {
[prop: number]: string;
}
1.7 可选属性与方法
在默认情况下,接口中属性签名和方法签名定义的对象属性都是必选的。在给接口类型赋值时,如果未指定必选属性则会产生编译错误。
我们可以在属性名或方法名后添加一个问号“?”,从而将该属性或方法定义为可选的。语法如下:
PropertyName?: Type
PropertyName?(ParameterList): Type
eg:
interface Foo {
x?: string;
y?(): number;
}
const a: Foo = {}
const b: Foo = { x: 'hi' }
const c: Foo = { y() { return 0; } }
const d: Foo = { x: 'hi', y() { return 0; } }
如果接口中定义了重载方法,那么所有重载方法签名必须同时为必选的或者可选的。
1.8 只读属性与方法
在接口声明中,使用readonly修饰符能够定义只读属性。readonly修饰符只允许在属性签名和索引签名中使用,具体语法如下所示:
readonly PropertyName: Type;
readonly [IndexName: string]: Type
readonly [IndexName: number]: Type
eg :
interface A {
readonly a: string;
readonly [prop: string]: string;
readonly [prop: number]: string;
}
1.9 接口的继承
接口可以继承其他的对象类型,这相当于将继承的对象类型中的类型成员复制到当前接口中。接口可以继承的对象类型如下:
- 接口。
- 对象类型的类型别名。
- 类。
- 对象类型的交叉类型。
接口的继承需要使用extends关键字。eg:
interface Style {
color: string;
}
interface Shape {
name: string;
}
//一个接口可以同时继承多个接口,父接口名之间使用逗号分隔
interface Circle extends Style, Shape {
radius: number;
}
const c: Circle = {
color: 'red',
name: 'circle',
radius: 1
};
2. 类型别名
如同接口声明能够为对象类型命名,类型别名声明则能够为TypeScript中的任意类型命名。
类型别名声明能够定义一个类型别名,eg:
type AliasName = Type
type是声明类型别名的关键字;AliasName表示类型别名的名称;Type表示类型别名关联的具体类型。
eg:
// 声明了一个类型别名Point,它表示包含两个属性的对象类型:
type Point = { x: number; y: number };
类型别名引用的类型可以为任意类型,例如原始类型、对象类型、联合类型和交叉类型等。示例如下:
type StringType = string;
type BooleanType = true | false;
type Point = { x: number; y: number; z?: number };
在程序中,可能会有一些比较复杂的或者书写起来比较长的类型,这时我们就可以声明一个类型别名来引用该类型,这也便于我们对这个类型进行重用。例如,下例中的DecimalDigit类型比较长,如果在每个引用该类型的地方都完整地写出该类型会很不方便。使用类型别名不但能够简化代码,还能够给该类型起一个具有描述性的名字。eg:
type DecimalDigit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
const digit: DecimalDigit = 6;
类型别名与接口相似,它们都可以给类型命名并通过该名字来引用表示的类型。虽然在大部分场景中两者是可以互换使用的,但类型别名和接口之间还是存在一些差别。
- 类型别名能够表示非对象类型,而接口则只能表示对象类型。因此,当我们想要表示原始类型、联合类型和交叉类型等类型时只能使用类型别名。
01 type NumericType = number | bigint;
- 接口可以继承其他的接口、类等对象类型,而类型别名则不支持继承
- 接口名总是会显示在编译器的诊断信息(例如,错误提示和警告)和代码编辑器的智能提示信息中,而类型别名的名字只在特定情况下才会显示出来。
- 接口具有声明合并的行为,而类型别名则不会进行声明合并。
interface A {
x: number;
}
interface A {
y: number;
}
定义了两个同名接口A,最终这两个接口中的类型成员会被合并。合并后的接口A如下所示:
interface A {
x: number;
y: number;
}
3. 类
JavaScript是一门面向对象的编程语言,它允许通过对象来建模和解决实际问题。同时,JavaScript也支持基于原型链的对象继承机制。虽然大多数的面向对象编程语言都支持类,但是JavaScript语言在很长一段时间内都没有支持它。在JavaScript程序中,需要使用函数来实现类的功能。
在ECMAScript 2015规范中正式地定义了类。同时,TypeScript语言也对类进行了全面的支持。
3.1 类的定义
虽然JavaScript语言支持了类,但其本质上仍是函数,类是一种语法糖。TypeScript语言对JavaScript中的类进行了扩展,为其添加了类型支持,如实现接口、泛型类等。
定义一个类需要使用class关键字。类似于函数定义,类的定义也有以下两种方式:
- 类声明
语法如下:
class ClassName {
// ...
}
eg:
//声明了一个Circle类,它包含一个number类型的radius属性。使用new关键字能够创建类的实例。与函数声明不同的是,类声明不会被提升,因此必须先声明后使用。
class Circle {
radius: number;
}
const c = new Circle();
在使用类声明时,不允许声明同名的类,否则将产生错误。
- 类表达式
语法如下:
//class是关键字;Name表示引用了该类的变量名;ClassName表示类的名字。在类表达式中,类名ClassName是可选的。
const Name = class ClassName {
// ...
};
3.2 成员变量
直接上栗子:
//在构造函数里将radius成员变量的值初始化为1。同时注意,在构造函数中引用成员变量时需要使用this关键字。
class Circle {
radius: number;
//只读成员变量必须在声明时初始化或在构造函数里初始化。
readonly a = 0;
constructor() {
this.radius = 1;
}
}
3.3 成员函数
成员函数也称作方法,声明成员函数与在对象字面量中声明方法是类似的。示例如下:
class Circle {
radius: number = 1;
area(): number {
return Math.PI * this.radius * this.radius;
}
}
3.4 成员存取器
成员存取器由get和set方法构成,并且会在类中声明一个属性。这和Java、C#等强类型语言是一样的。
存取器是实现数据封装的一种方式,它提供了一层额外的访问控制。类可以将成员变量的访问权限制在类内部,在类外部通过存取器方法来间接地访问成员变量。在存取器方法中,还可以加入额外的访问控制等处理逻辑。示例如下:
class Circle {
private _radius: number = 0;
get radius(): number {
return this._radius;
}
set radius(value: number) {
if (value >= 0) {
this._radius = value;
}
}
}
const circle = new Circle();
circle.radius; // 0
circle.radius = -1;
circle.radius; // 0
circle.radius = 10;
circle.radius; // 10
3.5 索引成员
类的索引成员会在类的类型中引入索引签名。索引签名包含两种,分别为字符串索引签名和数值索引签名。在实际应用中,定义类的索引成员并不常见。
在类的索引成员上不允许定义可访问性修饰符,如public和private等。
class A {
x: number = 0;
[prop: string]: number;
[prop: number]: number;
}
3.6 成员可访问性
成员可访问性定义了类的成员允许在何处被访问。TypeScript为类成员提供了以下三种可访问性修饰符:
- public
类的公有成员没有访问限制,可以在当前类的内部、外部以及派生类的内部访问。 - protected
类的受保护成员允许在当前类的内部和派生类的内部访问,但是不允许在当前类的外部访问。 - private
类的私有成员只允许在当前类的内部被访问,在当前类的外部以及派生类的内部都不允许访问。
3.7 构造函数
构造函数用于创建和初始化类的实例。当使用new运算符调用一个类时,类的构造函数就会被调用。构造函数以constructor作为函数名。eg:
class Circle {
radius: number;
constructor(r: number) {
this.radius = r;
}
}
const c = new Circle(1);
与普通函数相同,在构造函数中也可以定义可选参数、默认值参数和剩余参数。但是构造函数不允许定义返回值类型,因为构造函数的返回值类型永远为类的实例类型。
3.8 参数成员
TypeScript提供了一种简洁语法能够把构造函数的形式参数声明为类的成员变量,它叫作参数成员。在构造函数参数列表中,为形式参数添加任何一个可访问性修饰符或者readonly修饰符,该形式参数就成了参数成员,进而会被声明为类的成员变量。eg :
class A {
constructor(public x: number,readonly y: number,) {
//todo
}
}
const a = new A(0);
a.x; // 值为0
3.9 继承
继承是面向对象程序设计的三个基本特征之一,TypeScript中的类也支持继承。在定义类时可以使用extends关键字来指定要继承的类
class DerivedClass extends BaseClass { }
-
重写基类成员
在派生类中可以重写基类的成员变量和成员函数。在重写成员变量和成员函数时,需要在派生类中定义与基类中同名的成员变量和成员函数
class Shape {
color: string = 'black';
switchColor() {
this.color =
this.color === 'black' ? 'white' : 'black';
}
}
class Circle extends Shape {
color: string = 'red';
switchColor() {
this.color = this.color === 'red' ? 'green' : 'red';
}
}
const circle = new Circle();
circle.color; // 'red'
circle.switchColor();
circle.color; // 'green'
-
派生类实例化
在派生类的构造函数中必须调用基类的构造函数,否则将不能正确地实例化派生类。在派生类的构造函数中使用“super()”语句就能够调用基类的构造函数。
class Shape {
color: string = 'black';
constructor() {
this.color = 'black';
}
switchColor() {
this.color =
this.color === 'black' ? 'white' : 'black';
}
}
class Circle extends Shape {
radius: number;
constructor() {
super();
this.radius = 1;
}
}
在实例化派生类时的初始化顺序如下:
1)初始化基类的属性。
2)调用基类的构造函数。
3)初始化派生类的属性。
4)调用派生类的构造函数。
-
单继承
TypeScript中的类仅支持单继承,不支持多继承。也就是说,在extends语句中只能指定一个基类。 -
接口继承类
TypeScript允许接口继承类。若接口继承了一个类,那么该接口会继承基类中所有成员的类型。
在接口继承类时,接口不但会继承基类的公有成员类型,还会继承基类的受保护成员类型和私有成员类型。
3.10 实现接口
虽然一个类只允许继承一个基类,但是可以实现一个或多个接口。在定义类时,使用implements语句能够声明类所实现的接口。当实现多个接口时,接口名之间使用逗号“,”分隔。下例中,类C实现了接口A和接口B:
interface A {}
interface B {}
class C implements A, B {}
如果类的定义中声明了要实现的接口,那么这个类就需要实现接口中定义的类型成员。
3.11 静态成员
类的定义中可以包含静态成员。类的静态成员不属于类的某个实例,而是属于类本身。类的静态成员使用static关键字定义,并且只允许通过类名来访问。
class Circle {
static version: string = '1.0';
}
// 正确,结果为 '1.0'
const version = Circle.version;
3.12 抽象类和抽象成员
TypeScript也支持定义抽象类和抽象类成员。抽象类和抽象类成员都使用abstract关键字来定义。
抽象类与具体类的一个重要区别是,抽象类不能被实例化。也就是说,不允许使用new运算符来创建一个抽象类的实例。
抽象类的作用是作为基类使用,派生类可以继承抽象类。
抽象类也可以继承其他抽象类。
抽象类中允许(通常)包含抽象成员,也允许包含非抽象成员。
abstract class Base {
abstract a: string;
b: string = '';
}
在抽象类中允许声明抽象成员,抽象成员不允许包含具体实现代码。
如果一个具体类继承了抽象类,那么在具体的派生类中必须实现抽象类基类中的所有抽象成员。因此,抽象类中的抽象成员不能声明为private,否则将无法在派生类中实现该成员。
若没有正确地在具体的派生类中实现抽象成员,将产生编译错误。
3.13 this类型
在类中存在一种特殊的this类型,它表示当前this值的类型。我们可以在类的非静态成员的类型注解中使用this类型。