0%

TypeScript教程二之接口

上一节学习了 TypeScript 的基本类型,本节再来学习下接口 Interfaces 的使用。

TypeScript 的一个重要的特性就是帮助检查参数的字段是否是合法的,比如检查某个参数包含字段 foo,且类型需要是 number 类型,否则就会报错。通过接口,即 Interface 我们可以方便地实现这个操作。

第一个 Interface

最简单的实现方式参考下面的例子:

1
2
3
4
5
6
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

在这里我们声明了一个方法,叫做 printLabel,它接收一个参数叫做 labeledObj,在 labeledObj 后面声明了该参数需要的字段和类型,这里它需要一个字段 label,且类型必须要是 string 类型。在调用时,我们传入了一个 Object,它包含了两个字段,一个是 size,类型为 number,另一个是 label,类型为 string。这里值得注意的是传入的参数是比声明的参数多了一个字段 size 的,但是这并没有关系,编译器只检查传入的参数是否至少存在所需要的属性,对于多余的属性是不关心的。

运行结果如下:

1
[LOG]: "Size 10 Object"

如果此时我们将 label 属性的类型修改为 number:

1
2
3
function printLabel(labeledObj: { label: number }) {
console.log(labeledObj.label);
}

则会出现如下报错:

1
Argument of type '{ size: number; label: string; }' is not assignable to parameter of type '{ label: number; }'. Types of property 'label' are incompatible. Type 'string' is not assignable to type 'number'.

这里就提示 label 属性只能传入 number 类型,而不能是 string 类型。

但上面这个写法其实很不友好,如果属性比较多,那这个声明会非常复杂,而且不同方法如果都用到这个参数,难道还把它的声明都重复声明一遍?这也太不好了吧。

所以,为了更方便地实现声明,这里我们可以使用 Interface 来实现,上面的例子就可以改写为如下形式:

1
2
3
4
5
6
7
8
9
10
interface LabeledValue {
label: string;
}

function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

这里我们使用 interface 声明了一个类型声明,这样在 printLabel 就可以直接使用 Interface 的名称了。

怎么样?这种写法是不是感觉好多了。

Optional properties

某些情况下,某些字段并不是完全必要的,比如看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
interface LabeledValue {
label: string;
count: number,
message: string,
}

function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}

let myObj = { size: 10, count: 1, label: "Size 10 Object"};
printLabel(myObj);

其中 message 字段其实在 myObj 对象里面没有,而且这个字段也并不是必需的,但是该字段如果存在的话,必须是 string 类型。那像上面的写法,其实就会报错了:

1
Argument of type '{ size: number; count: number; label: string; }' is not assignable to parameter of type 'LabeledValue'. Property 'message' is missing in type '{ size: number; count: number; label: string; }' but required in type 'LabeledValue'.

这里说 message 字段没有传。

这时候我们可以将 message 标识为可选字段,只需要在字段后面加个 ? 就好了,写法如下:

1
2
3
4
5
interface LabeledValue {
label: string;
count: number,
message?: string,
}

这样就不会再报错了。

Readonly properties

在某些情况下,我们期望一个 Object 的某些字段不能后续被修改,只能在创建的时候声明,这个怎么做到呢?很简单,将其设置为只读字段就好了,示例如下:

1
2
3
4
5
6
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

这里我们声明了一个名为 Point 的 Interface,然后在创建 Point 的时候将 x 设置为 10。但后续如果我们想设置 x 的属性为 5 的时候,就会报错了:

1
Cannot assign to 'x' because it is a read-only property.

这样就可以保证某些字段不能在后续操作流程中被修改,保证了安全性。

另外我们可以使用 ReadonlyArray 来声明不可变的 Array,一旦初始化完成之后,后续所有关于 Array 的操作都会报错,示例如下:

1
2
3
4
5
6
7
8
9
10
11
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;

ro[0] = 12; // error!
Index signature in type 'readonly number[]' only permits reading.
ro.push(5); // error!
Property 'push' does not exist on type 'readonly number[]'.
ro.length = 100; // error!
Cannot assign to 'length' because it is a read-only property.
a = ro; // error!
The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.

另外到这里大家可能疑惑 readonly 和 const 是什么区别,二者不都代表不可修改吗?其实区分很简单,readonly 是用来修改 Object 的某个属性的,而 const 是用来修饰某个变量的。

Function Types

除了用 interface 声明 Object 的字段,我们还可以声明方法的一些规范,示例如下:

1
2
3
interface SearchFunc {
(source: string, subString: string): boolean;
}

这里就是用 interface 声明了一个 Function,前半部分是接收的参数类型,后面 boolean 是返回值类型。

声明 interface 之后,我们便可以声明一个 Function 了,写法如下:

1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc = (source: string, subString: string) => {
let result = source.search(subString);
return result > -1;
};

这里声明了一个 Function 叫做 mySearch,其中其参数和返回值严格按照 SearchFunc 这个 Interface 来实现,那就没问题。

如果我们将返回值改掉,改成非 boolean 类型,示例如下:

1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc = (source: string, subString: string) => {
let result = source.search(subString);
return result;
};

这时候就会得到如下报错:

1
Type '(source: string, subString: string) => number' is not assignable to type 'SearchFunc'. Type 'number' is not assignable to type 'boolean'.

这里就说返回值是 number,而不是 boolean。

Class Types

除了声明 Function,interface 还可以用来声明 Class,主要作用就是声明 class 里面所必须的属性和方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}

class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}

这个简直跟其他语言的接口定义太像了。定义好了 ClockInterface 之后,class 需要使用 implements 来实现这个接口,同时必须要声明 currentTime 这个变量和 setTime 方法,类型也需要完全一致,不然就会报错。

Extending Interfaces

另外 Interface 之间也是可以继承的,相当于在一个 Interface 上进行扩展,示例如下:

1
2
3
4
5
6
7
8
9
10
11
interface Shape {
color: string;
}

interface Square extends Shape {
sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;

这里 Shape 这个 Interface 只有 color 这个属性,而 Square 则继承了 Shape,并且加了 sideLength 属性,那其实现在 Square 这个接口声明了 color 和 sideLength 这两个属性。

另外 Interface 还支持多继承,获取被继承的 Interface 的所有声明,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Shape {
color: string;
}

interface PenStroke {
penWidth: number;
}

interface Square extends Shape, PenStroke {
sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

但如果同名的字段不一致怎么办呢?比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Fruit {
color: string
}

interface Apple extends Fruit {
hasLeaf: boolean
}

interface Orange extends Fruit {
size: number,
hasLeaf: number
}

interface Watermalon extends Apple, Orange {

}

这里 hasLeaf 在 Apple 里面是 boolean 类型,在 Orange 里面是 number 类型,最后 Watermalon 继承了这两个 Interface 会怎样呢?

很明显,报错了,结果如下:

1
Interface 'Watermalon' cannot simultaneously extend types 'Apple' and 'Orange'. Named property 'hasLeaf' of types 'Apple' and 'Orange' are not identical.

意思就是说字段类型不一致。

所以,要多继承的话,需要被继承的 Interface 里面的属性不互相冲突,不然是无法同时继承的。

Interfaces Extending Classes

在某些情况下,Interface 可能需要继承 Class,Interface 扩展 Class 时,它将继承该 Class 的成员,但不继承其实现。这就类似该 Interface 声明了该类的所有成员而没有提供实现。

另外 Interface 甚至可以继承 Class 的私有成员和受保护成员。这意味着,当创建一个扩展带有私有或受保护成员的 Class 的 Interface 时,该 Interface 只能由该 Class 或其子 Class 实现。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Control {
private state: any;
}

interface SelectableControl extends Control {
select(): void;
}

class Button extends Control implements SelectableControl {
select() {}
}

class TextBox extends Control {
select() {}
}

class ImageControl implements SelectableControl {
// Error, Class 'ImageControl' incorrectly implements interface 'SelectableControl'.
// Types have separate declarations of a private property 'state'.
private state: any;
select() {}
}

上面我们可以知道,当创建一个扩展带有私有或受保护成员的 Class 的 Interface 时,该 Interface 只能由该 Class 或其子 Class 实现。在这里 ImageControl 由于没有继承 Control,但同时 Control 还包含了私有成员变量,所以 ImageControl 并不能继承得到 state 这个私有成员变量,所以会报错。

以上便是关于 Interface 的一些用法,后面会继续总结其他的用法,如 Functions、Classes 等详细用法。