TypeScript 接口

接口

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

function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}

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

可选属性

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ? 符号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface SquareConfig {
color?: string;
width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number} {
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}

let mySquare = createSquare({color: "black"});

只读属性

1
2
3
4
interface Point {
readonly x: number;
readonly y: number;
}

TypeScript具有ReadonlyArray类型,它与Array相似,只是把所有可变方法去掉了, 确保数组创建后再也不能被修改.

1
2
3
4
5
6
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:

1
a = ro as number[];

readonly vs const, 变量使用的话用 const,属性则使用readonly。

额外的属性检查

1
2
3
4
5
6
7
8
9
10
interface SquareConfig {
color?: string;
width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}

let mySquare = createSquare({ colour: "red", width: 100 });

如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误.即你传入的对象自变量中出现了配置中不包含的属性,那么就会触发 额外的属性检查 并提示错误。
虽然witdh已经确定,但是colour并不是 SquareConfig 想要的值,ts会判定这里有一个错误。

绕过的方式可以使用断言:

1
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

最佳的方式是能够添加一个字符串索引签名,

1
2
3
4
5
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}

还是可以考虑进行重新复制绕过:

1
2
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions); // 这里进行赋值之后会认定 squareOptions 并不是 squareConfig 类型,所以就不会出现错误信息。

函数类型

1
2
3
interface SearchFunc {
(source: string, subString: string): boolean;
}
1
2
3
4
5
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}

索引签名

我们可以明确的指定索引签名。例如:假设你想确认存储在对象中任何内容都符合 { message: string } 的结构,你可以通过 [index: string]: { message: string } 来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const foo: {
[index: string]: { message: string };
} = {};

// 储存的东西必须符合结构
// ok
foo['a'] = { message: 'some message' };

// Error, 必须包含 `message`
foo['a'] = { messages: 'some message' };

// 读取时,也会有类型检查
// ok
foo['a'].message;

// Error: messages 不存在
foo['a'].messages;
1
2
3
4
5
6
7
8
interface StringArray {
[index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

索引签名的名称(如:{ [index: string]: { message: string } } 里的 index )除了可读性外,并没有任何意义。例如:如果有一个用户名,你可以使用 { username: string}: { message: string },这有利于下一个开发者理解你的代码。

所有成员都必须符合字符串的索引签名

1
2
3
4
5
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
}

TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}

// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}

Class 类型

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

class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
Difference between the static and instance sides of classes
1
2
3
4
5
6
7
8
9
10
interface ClockConstructor {
new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
// Class 'Clock' incorrectly implements interface 'ClockConstructor'.
// Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.
currentTime: Date;
constructor(h: number, m: number) {}
}

继承接口

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

interface Square extends Shape {
sideLength: number;
}

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

Hybrid类型

接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}

function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

接口继承类

当接口继承了一个类类型时,它会继承类的成员但不包括其实现

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 {
// Class 'ImageControl' incorrectly implements interface 'SelectableControl'.
// Types have separate declarations of a private property 'state'.
private state: any;
select() {}
}

Interfaces inherit even the private and protected members of a base class. This means that when you create an interface that extends a class with private or protected members, that interface type can only be implemented by that class or a subclass of it.

如果接口继承的父类包含了私有属性,那么该接口就只能被 这个类或其子类所实现。
因为接口中存在了private属性,但是 ImageControl 中定义的 state 是自己的,所以会和 接口中的私有 state 冲突。
但是如果不定义,则又会缺少参数。所以导致了存在私有属性只有这个类或其子类所实现,这样就可以保证当前的类中 实现 接口中的 private 属性。

1
2
3
4
5
6
7
class ImageControl extends Control implements SelectableControl {
select() {}
}

class ImageControl extends TextBox implements SelectableControl {
select() {}
}