# interface
interface是对象的模板,可以看作是一种类型约定,中文译为接口。使用了某个模板的对象,就拥有了指定的类型结构。方括号运算符可以取出interface某个属性的类型。
赋值时,变量的形状必须和接口的形状保持一致。定义的变量比接口少一些属性、多一些属性都是不允许的。
interface Person {
firstName: string;
lastName: string;
age: number;
} // 任何实现这个接口的对象,都必须部署这三个属性,并且必须符合规定的类型
const p:Person = { // 变量p的类型就是接口Person,所以必须符合Person指定的结构
firstName: 'John',
lastName: 'Smith',
age: 25
}; // 实现该接口很简单,只要指定它作为对象的类型即可
type A = Person['age']; // number
2
3
4
5
6
7
8
9
10
11
interface可以表示对象的各种语法,它的成员有5种形式:
- 对象属性
- 分别使用冒号指定每个属性的类型。
- 属性之间使用分号或逗号分隔,最后一个属性结尾的分号或逗号可以省略。
- 如果属性是可选的,就在属性名后面加一个问号。
- 如果属性是只读的,需要加上readonly修饰符。
interface Point {
x?: string;
readonly y: string;
[prop: string]: number;
}
2
3
4
5
- 对象的属性索引
- 属性索引共有string、number和symbol三种类型。
- 一个接口中最多只能定义一个字符串索引。字符串索引会约束该类型中所有名字为字符串的属性。
- 属性的数值索引,其实是指定数组的类型。
- 一个接口中最多只能定义一个数值索引。数值索引会约束所有名称为数值的属性。
- 如果一个interface同时定义了字符串索引和数值索引,那么数值索引必须服从于字符串索引。因为在JavaScript中,数值属性名最终是自动转换成字符串属性名。
interface C {
[prop: number]: string;
}
const obj:C = ['a', 'b', 'c']; // 属性名的类型是数值,所以可以用数组对变量obj赋值
interface A {
[prop: string]: number;
[prop: number]: string; // 报错。数值索引的属性值类型与字符串索引不一致,就会报错
}
interface B {
[prop: string]: number;
[prop: number]: number; // 正确。数值索引必须兼容字符串索引的类型声明
}
2
3
4
5
6
7
8
9
10
11
12
- 对象方法
- 对象的方法共有三种写法。属性名可以采用表达式。
- 类型方法可以重载。interface里面的函数重载不需要给出实现。但是,由于对象内部定义方法时,无法使用函数重载的语法,所以需要额外在对象外部给出函数方法的实现。
// 写法一
interface A {
f(x: boolean): string;
}
// 写法二
interface B {
f: (x: boolean) => string;
}
// 写法三
interface C {
f: { (x: boolean): string };
}
// 属性名可以采用表达式,所以下面的写法也是可以的。
const f = 'f';
interface A {
[f](x: boolean): string;
}
interface A {
f(): number;
f(x: boolean): boolean;
f(x: string, y: string): string;
} // 函数重载,不需要给出实现,需要额外在对象外部给出函数方法的实现
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 函数
- interface 也可以用来声明独立的函数。
interface Add {
(x:number, y:number): number;
} // 声明了一个函数类型
const myAdd:Add = (x,y) => x + y;
2
3
4
- 构造函数
- interface 内部可以使用new关键字,表示构造函数。
interface ErrorConstructor {
new (message?: string): Error; // 内部有new命令,表示它是一个构造函数
}
2
3
- 有时我们希望一个接口允许有任意的属性,可以使用如下方式。
interface Person {
name: string;
age?: number;
[propName: string]: any;
} // 使用 [propName: string] 定义了任意属性取 string 类型的值。
interface Person {
name: string;
age?: number;
[propName: string]: string;
} // 一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
interface Person {
name: string;
age?: number;
[propName: string]: string | number;
} // 如果接口中有多个类型的属性,则可以在任意属性中使用联合类型
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# interface 的继承
# interface 继承 interface
interface可以使用extends关键字,继承其他interface。extends关键字会从继承的接口里面拷贝属性类型。这样就不必书写重复的属性。
interface 允许多重继承。多重接口继承,实际上相当于多个父接口的合并。多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错。
interface Style {
color: string;
}
interface Shape {
name: string;
}
interface Circle extends Style, Shape {
radius: number;
} // Circle同时继承了Style和Shape,所以拥有三个属性color、name和radius
2
3
4
5
6
7
8
9
如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。
interface Foo {
id: string;
}
interface Bar {
id: number;
}
// 报错
interface Baz extends Foo, Bar {
type: string;
} // Baz同时继承了Foo和Bar,但是后两者的同名属性id有类型冲突
2
3
4
5
6
7
8
9
10
# interface 继承 type
interface可以继承type命令定义的对象类型。如果type命令定义的类型不是对象,interface就无法继承。
type Country = {
name: string;
capital: string;
}
interface CountryWithPop extends Country {
population: number;
} // CountryWithPop继承了type命令定义的Country对象,并且新增了一个population属性
2
3
4
5
6
7
# interface 继承 class
interface还可以继承class,即继承该类的所有成员。某些类拥有私有成员和保护成员,interface可以继承这样的类,但无法用于对象,意义不大。
class A { // A有私有成员和保护成员
private x: string = '';
protected y: string = '';
}
interface B extends A { // B继承了A,但无法用于对象,因为对象不能实现这些成员
z: number
}
// 报错
const b:B = { /* ... */ }
// 报错
class C implements B { //致B只能用于其他class
// ...
} // 这时其他class与A之间不构成父类和子类的关系,使得x与y无法部署
2
3
4
5
6
7
8
9
10
11
12
13
# 接口合并
多个同名接口会合并成一个接口。JavaScript开发者常常对全局对象或者外部库,添加自己的属性和方法。只要使用interface给出这些自定义属性和方法的类型,就能自动跟原始的interface合并,使得扩展外部类型非常方便。
同名接口合并时,同一个属性如果有多个类型声明,彼此不能有类型冲突。
同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。但是,如果有一个参数是字面量类型,字面量类型的优先级会排到最前面。
如果两个interface组成的联合类型存在同名属性,那么该属性的类型也是联合类型。
接口的合并:接口中的属性在合并时会简单的合并到一个接口中,并的属性的类型必须是唯一的。
# interface 与 type 的异同
interface命令与type命令作用类似,都可以表示对象类型。很多对象类型既可用interface表示,也可用type表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。
它们的相似之处,表现在都能为对象类型起名。
type Country = {
name: string;
capital: string;
}
interface Country {
name: string;
capital: string;
}
2
3
4
5
6
7
8
class命令也有类似作用,通过定义一个类,同时定义一个对象类型。但是,它会创造一个值,编译后依然存在。如果只是单纯想要一个类型,应该使用type或interface。
interface 与 type 的区别有下面几点:
- type能够表示非对象类型,而interface只能表示对象类型(包括数组、函数等)。
- interface可以继承其他类型,type不支持继承。
- 继承的主要作用是添加属性,type定义的对象类型如果想要添加属性,只能使用&运算符,重新定义一个类型。(&运算符表示同时具备两个类型的特征,可以起到两个对象类型合并的作用。)
- interface添加属性,采用的是继承的写法。继承时,type 和 interface 是可以换用的。interface 可以继承 type。type 也可以继承 interface。
- 同名interface会自动合并,同名type则会报错。即TS不允许使用type多次定义同一个类型。
- interface不能包含属性映射(mapping),type可以。
- this关键字只能用于interface。
- type可以扩展原始数据类型,interface 不行。
- interface无法表达某些复杂类型(比如交叉类型和联合类型),但是type可以。
type Foo = { x: number; };
interface Bar extends Foo { // interface 可以继承 type
y: number;
}
interface Foo {
x: number;
}
type Bar = Foo & { y: number; }; // type 也可以继承 interface
2
3
4
5
6
7
8
如果有复杂的类型运算,那么没有其他选择只能使用type;一般情况下,interface灵活性比较高,便于扩充类型或自动合并,建议优先使用。
# 类
类是面向对象编程的基本构件,封装了属性和方法,TypeScript 给予了全面支持。
这里对类相关的概念做一个简单的介绍。 类(Class):定义了一件事物的抽象特点,包含它的属性和方法 对象(Object):类的实例,通过 new 生成 面向对象(OOP)的三大特性:封装、继承、多态 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat 存取器(getter & setter):用以改变属性的读取和赋值行为 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
# 属性的类型
类的属性可以在顶层声明,也可以在构造方法内部声明。对于顶层声明的属性,可以在声明时同时给出类型。
如果不给出类型,TypeScript 会认为是any。如果声明时给出初值,可以不写类型,TypeScript 会自行推断属性的类型。
TypeScript 有一个配置项strictPropertyInitialization,只要打开(默认是打开的),就会检查属性是否设置了初值,如果没有就报错。
如果类的顶层属性不赋值,就会报错。如果不希望出现报错,可以使用非空断言。就是说,在属性名后面添加感叹号,表示这两个属性肯定不会为空,TypeScript就不报错了。
class Point {
x!: number;
y!: number;
}
// 打开 strictPropertyInitialization
class Point {
x: number; // 报错
y: number; // 报错
}
2
3
4
5
6
7
8
9
# readonly 修饰符
属性名前面加上readonly修饰符,就表示该属性是只读的。实例对象不能修改这个属性。
readonly 属性的初始值,可以写在顶层属性,也可以写在构造方法里面。
构造方法内部设置只读属性的初值、修改只读属性的值,都是可以的。或者说,如果两个地方都设置了只读属性的值,以构造方法为准。在其他方法修改只读属性都会报错。
如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面。
修饰符和readonly还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁。readonly只允许出现在属性声明或索引签名或构造函数中。
# 方法的类型
类的方法就是普通函数,类型声明方式与函数一致。
类的方法跟普通函数一样,可以使用参数默认值,以及函数重载。
构造方法可以接受一个参数,也可以接受两个参数,采用函数重载进行类型声明。另外,构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象。
# 存取器方法
存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。它们用于读写某个属性,取值器用来读取属性,存值器用来写入属性。
class C {
_name = '';
get name() { // 取值器,其中get是关键词,name是属性名
return this._name;
} // 外部读取name属性时,实例对象会自动调用这个方法,该方法的返回值就是name属性的值
set name(value) { // 存值器,其中set是关键词,name是属性名
this._name = value;
} // 外部写入name属性时,实例对象会自动调用这个方法,并将所赋的值作为函数参数传入
}
2
3
4
5
6
7
8
9
TypeScript 对存取器有以下规则。
- 如果某个属性只有get方法,没有set方法,那么该属性自动成为只读属性。
- TS 5.1版之前,set方法的参数类型,必须兼容get方法的返回值类型,否则报错。TS5.1 版做出了改变,现在两者可以不兼容。
- get方法与set方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。
# 属性索引
类允许定义属性索引。
class MyClass {
[s:string]: boolean |
((s:string) => boolean); //所有属性名类型为字符串的属性,属性值要么是布尔值,要么是返回布尔值的函数
get(s:string) {
return this[s] as boolean;
}
}
2
3
4
5
6
7
注意,由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。
class MyClass {
[s:string]: boolean | (() => boolean);
f() {
return true;
}
}
2
3
4
5
6
属性存取器视同属性。
class MyClass {
[s:string]: boolean; // 属性索引虽然没有涉及方法类型,但是不会报错
get isInstance() { // 读取器虽然是一个函数方法,但是视同属性
return true;
}
}
2
3
4
5
6
# 类的 interface 接口
有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口,用 implements 关键字来实现。
# implements 关键字
interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。
interface只是指定检查条件,如果不满足这些条件就会报错。它并不能代替class自身的类型声明。比如,类B实现了接口A,但A并不能代替B的类型声明,B类依然需要声明参数的类型,需要声明可选属性。
类可以定义接口没有声明的方法和属性。表示除了满足接口给出的条件,类还有额外的条件。
implements关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。在接口继承类的时候,也只会继承它的实例属性和实例方法。
注意,interface描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为TypeScript设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法。
# 实现多个接口
一个类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。但是,同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代。第一种方法是类的继承。第二种方法是接口的继承。在 TypeScript 中,接口与接口之间可以是继承关系。
class Car implements MotorVehicle {
}
class SecretCar extends Car implements Flyable, Swimmable { // 类的继承
}
interface A {
a:number;
}
interface B extends A { // 接口的继承
b:number;
}
2
3
4
5
6
7
8
9
10
注意,发生多重实现时(即一个接口同时实现多个接口),不同接口不能有互相冲突的属性。
# 类与接口的合并
TypeScript不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。注意,合并进类的非空属性,如果在赋值之前读取,会返回undefined。
类的合并:类的合并与接口的合并规则一致。
# Class 类型
# 实例类型
TypeScript的类本身就是一种类型,但是它代表该类的实例类型,而不是class的自身类型。
对于引用实例对象的变量来说,既可以声明类型为 Class,也可以声明类型为 Interface,因为两者都代表实例对象的类型。
interface MotorVehicle {
}
class Car implements MotorVehicle {
}
// 写法一
const c1:Car = new Car();
// 写法二
const c2:MotorVehicle = new Car();
2
3
4
5
6
7
8
作为类型使用时,类名只能表示实例的类型,不能表示类的自身类型。
由于类名作为类型使用,实际上代表一个对象,因此可以把类看作为对象类型起名。事实上,TypeScript有三种方法可以为对象类型起名:type、interface 和 class。
# 类的自身类型
类的自身类型就是一个构造函数,可以单独定义一个接口来表示。要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符。
类只是构造函数的一种语法糖,本质上是构造函数的另一种写法。所以,类的自身类型可以写成构造函数的形式。构造函数也可以写成对象形式。
可以把构造函数提取出来,单独定义一个接口,这样可以大大提高代码的通用性。
interface PointConstructor {
new(x:number, y:number):Point;
}
function createPoint(
PointClass: PointConstructor,
x: number,
y: number
):Point {
return new PointClass(x, y);
}
2
3
4
5
6
7
8
9
10
# 结构类型原则
Class也遵循结构类型原则。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。
如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。
只要 A 类具有 B 类的结构,哪怕还有额外的属性和方法,TypeScript也认为 A 兼容 B 的类型。
不仅是类,如果某个对象跟某个 class 的实例结构相同,TypeScript也认为两者的类型相同。由于这种情况,运算符instanceof不适用于判断某个对象是否跟某个 class 属于同一类型。
空类不包含任何成员,任何其他类都可以看作与空类结构相同。因此,凡是类型为空类的地方,所有类(包括对象)都可以使用。
注意,确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。
如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TypeScript 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系。
# 类的继承
类(这里又称子类)可以使用extends关键字继承另一个类(这里又称基类)的所有属性和方法。一般来讲,一个类只能继承自另一个类。
根据结构类型原则,子类也可以用于类型为基类的场合。子类可以覆盖基类的同名方法。
使用super关键字指代基类是常见做法。
子类的同名方法不能与基类的类型定义相冲突。
如果基类包括保护成员(protected修饰符),子类可以将该成员的可访问性设置为公开(public修饰符),也可以保持保护成员不变,但是不能改用私有成员(private修饰符)。
extends关键字后面不一定是类名,可以是一个表达式,只要它的类型是构造函数就可以了。
对于那些只设置了类型、没有初值的顶层属性,有一个细节需要注意。没有设置初值,代码在不同的编译设置下编译结果不一样。解决方法就是使用declare命令,去声明顶层成员的类型,告诉 TS这些成员的赋值由基类实现。
# 可访问性修饰符
类的内部成员的外部可访问性,由三个可访问性修饰符控制:public、private和protected。这三个修饰符的位置,都写在属性或方法的最前面。
# public
public修饰符表示这是公开成员,*外部可以自由访问*****。是默认修饰符,如果省略不写,实际上就带有该修饰符。因此,类的属性和方法默认都是外部可访问的。正常情况下,除非为了醒目和代码可读性,public都是省略不写的。
- public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
# private
private修饰符表示私有成员,*只能用在当前类的内部*****,*类的实例和子类都不能使用*****该成员。如果在类的内部,当前类的实例可以获取私有成员。
- private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
严格地说,private定义的私有成员,并不是真正意义的私有成员。一方面,编译成JavaScript后,private关键字就被剥离了,这时外部访问该成员就不会报错。另一方面,由于前一个原因,TypeScript对于访问private成员没有严格禁止,使用方括号写法([])或者in运算符,实例对象就能访问该成员。 建议不使用private,改用 ES2022 的写法(属性名前加#),获得真正意义的私有成员。
构造方法也可以是私有的,这就直接防止了使用new命令生成实例对象(实现了单例模式),只能在类的内部创建实例对象。当构造函数修饰为 private 时,该类不允许被继承或者实例化,当构造函数修饰为 protected 时,该类只允许被继承。
# protected
protected修饰符表示该成员是保护成员,*只能在类的内部使用****该成员,*实例无法使用***该成员,但是*子类内部可以使用****。子类不仅可以拿到父类的保护成员,还可以定义同名成员。在类的外部,实例对象不能读取保护成员,但是在类的内部可以。
- protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
# 实例属性的简写形式
实际开发中,很多实例属性的值,是通过构造方法传入的。
class Point {
x:number;
y:number;
constructor(x:number, y:number) { // 属性x和y的值是通过构造方法的参数传入的
this.x = x;
this.y = y;
}
}
2
3
4
5
6
7
8
这样的写法等于对同一个属性要声明两次类型,一次在类的头部,另一次在构造方法的参数里面。这有些累赘,TypeScript 就提供了一种简写形式。
class Point {
constructor(
public x:number, //这里的public不能省略
public y:number
) {}
}
const p = new Point(10, 10);
p.x // 10
p.y // 10
2
3
4
5
6
7
8
9
构造方法的参数x前面有public修饰符,这时 TypeScript 就会自动声明一个公开属性x,不必在构造方法里面写任何代码,同时还会设置x的值为构造方法的参数值。
除了public修饰符,构造方法的参数名只要有private、protected、readonly修饰符,都会自动声明对应修饰符的实例属性。readonly还可以与其他三个可访问性修饰符,一起使用。
# 静态成员
类的内部可以使用static关键字,定义静态成员。静态成员是只能通过类本身使用的成员,不能通过实例对象使用。static关键字前面可以使用 public、private、protected 修饰符。静态私有属性也可以用ES6语法的 #前缀 表示。public和protected的静态成员可以被继承。
# 泛型类
类也可以写成泛型,使用类型参数。注意,静态成员不能使用泛型的类型参数。
class Box<Type> { // 类Box有类型参数Type,因此属于泛型类。
contents: Type;
constructor(value:Type) {
this.contents = value;
}
}
// 新建实例时,变量的类型声明需要带有类型参数的值,不过本例等号左边的Box<string>可以省略不写
const b:Box<string> = new Box('hello!'); // 因为可以从等号右边推断得到
2
3
4
5
6
7
8
# 抽象类,抽象成员
TypeScript允许在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做抽象类。抽象类只能当作基类使用,用来在它的基础上定义子类。
抽象类是不允许被实例化的。抽象类中的抽象方法必须被子类实现。即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类。
抽象类的作用是,确保各种相关的子类都拥有跟基类相同的接口,可以看作是模板。其中的抽象成员都是必须由子类实现的成员,非抽象成员则表示基类已经实现的、由所有子类共享的成员。
抽象类的子类也可以是抽象类,也就是说,抽象类可以继承其他抽象类。
抽象类的内部可以有已经实现好的属性和方法,也可以有未实现的属性和方法。后者叫抽象成员,即属性名和方法名有abstract关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。
这里有几个注意点。
- 抽象成员只能存在于抽象类,不能存在于普通类。
- 抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加abstract关键字。
- 抽象成员前也不能有private修饰符,否则无法在子类中实现该成员。
- 一个子类最多只能继承一个抽象类。
# this 问题
类的方法经常用到this关键字,它表示该方法当前所在的对象。
TypeScript 允许函数增加一个名为this的参数,放在参数列表的第一位,用来描述函数内部的this关键字的类型。编译时,TypeScript 一旦发现函数的第一个参数名为this,则会去除这个参数,即编译结果不会带有该参数。
this参数的类型可以声明为各种对象。
在类的内部,this本身也可以当作类型使用,表示当前类的实例对象。
TypeScript提供了一个noImplicitThis编译选项。如果打开了这个设置项,如果this的值推断为any类型,就会报错。
注意,this类型不允许应用于静态成员。静态成员拿不到实例对象。
有些方法返回一个布尔值,表示当前的this是否属于某种类型。这时,这些方法的返回值类型可以写成this is Type的形式,其中用到了is运算符。