0%

typescript 接口

TypeScript的核心原则是对所有值具有的结构进行类型检查,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。比如:

1
2
3
4
5
6
7
interface HeroInterface {
name: string;
zh_name: string;
}
function createHero (hero: HeroInterFace) {
console.log(hero.name);
}

我们定义的HeroInterFace接口好比一个名字,用来描述我们的需要参数的样子,在这里我们可以看到,我们需要个有属性namezh_name属性的对象,且对象属性的类型也要正确。如果传入的对象满足上面的条件,那么它就是被允许的。

接口的定义:

1
2
3
4
interface Interface_name {
property1: type;
property2: type;
}

接口的属性

上面我们定义的接口HeroInterface里面有两个属性都是必须的,在方法createHero调用的时候会严格检查对象是否包含这两个属性,是必须包含的。

除了严格检查的属性,还有可选的属性、只读属性以及需要额外检查的属性。

可选属性

接口中的属性不全都是必须的,有些只是在某些条件下存在,或者根本不存在,可选属性应用在给函数传入参数对象中只有部分属性时很常用。

例如,我们修改上面的HeroInterface接口,使属性zh_name为可选属性,这样我们传递给方法createHero时并不会检查形参里面的对象是否包含有zh_name属性:

1
2
3
4
5
6
7
8
9
interface HeroInterFace {
name: string;
zh_name?: string;
}
function createHero (hero: HeroInterFace) {
console.log(hero.name);
}
let testHero = { name: 'Iron Man'};
createHero(testHero);

可选属性的有两个好处:

  1. 可以对可能存在的属性进行预定义;
  2. 可以捕获引用了不存在的属性时的错误;

比如,当我们访问不存在的属性时,会报错:

1
2
3
4
function createHero (hero: HeroInterFace) {
console.log(hero.zh_name); // ok
console.log(hero.zh_name1); // erro: HeroInterface have no property named zh_name1
}

只读属性

一些对象属性只能在对象刚刚创建的时候赋予其值,在运行过程中无法修改,可以用readonly来指定只读属性。比如,我们给接口HeroInterface添加一个只读属性gender

1
2
3
4
5
6
7
8
9
10
interface HeroInterFace {
name: string;
zh_name?: string;
readonly gender: string;
}

function createHero (hero: HeroInterFace) {
console.log(hero.zh_name);
hero.gender = 'woman'; // error 无法修改readonly属性
}

当我们视图修改一个只读属性的时候,会错误提示。

额外的属性检查

当我们传递的参数对象中不存在已定义的属性的时候,TypeScript会给个错误提示,这样是有好处的。但有些情况我们是希望传入一些未定义属性,处理这种情况可以使用类型断言,也可以将这个对象赋值给另一个变量,但最佳的方式是给这个接口添加一个索引签名,表示这个接口可以有任意属性的任意属性:

1
2
3
4
5
6
interface HeroInterFace {
name: string;
zh_name?: string;
readonly gender: string;
[proName: string]: any;
}

虽然对于包含方法和内部状态复杂的字面量来讲,可能真的需要这样的技巧来规避这种错误提示,但大多数情况下额外属性检查的错误是真正的bug,所以如果需要某些属性,还是应该去接口里面声明下。

函数类型

除了能够描述普通的带属性的对象外,接口还可以描述函数的类型。

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。定义函数的参数列表和返回值类型。参数里面的每个参数都需要名字和类型。

1
2
3
interface CreateHeroFunc {
(hero: HeroInterFace): boolean;
}

定义好后我们可以使用这个接口创建一个函数,函数的参数名不需要与接口里定义的名字相匹配:

1
2
3
4
5
let myCreateHero: CreateHeroFunc;
myCreateHero = function(test): boolean {
console.log(test.name);
return false;
};

比如我们定义多个参数的函数(可以使用箭头函数定义):

1
2
3
4
5
6
7
type MoreParamsFunc = (name: string, age: number) => boolean;
let moreParams: MoreParamsFunc;
moreParams = function(name1, age2): boolean {
name1 ++; // error: An arithmetic operand must be of type 'any', 'number' or an enum type.
age2 ++; // ok
return false;
}

可以发现,虽然函数名可以不是接口里面定义的名字,但是函数的参数会逐个检查参数的类型和参数属性的类型,不匹配的时候会报错。

可索引的类型

我们也可以描述你能够通过索引得到的类型,比如a[10]a['test']。比如,我们来定义一个可以索引的字符串数组:

1
2
3
4
5
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ['test', 'fdsfd', 'string'];

定义的接口表示,当我们用number去索引时,会得到string类型的值。

共有两种索引签名:字符串和数字。可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串返回值类型的子类型,因为当使用数字索引时,JavaScript会把它转换为字符串然后再去索引对象,因此两者需要保持一致,比如:

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

interface NotOkay {
[x: number]: Animal; // error: 数字类型的索引类型‘animal’无法分配给字符串类型的索引类型'dog'
[x: string]: Dog;
}

字符串索引签名能够很好的描述dictionary模式,并且它们也会确保素有属性与其返回值类型相匹配。因为字符串索引声明了obj.propertyobj['property']两种形式都可以。比如:

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

这个接口定义了字符串的索引类型,并且索引的返回值类型是number,这样在检查到name属性的时候,会报告属性类型错误。

可以将索引签名设置为只读,这样就防止了给索引赋值,实现了保护数组只读:

1
2
3
4
5
6
interface StringArray {
readonly [index: number]: string;
}
let myArray: StringArray;
myArray = ['tony', 'fdsfds', 'fdsfsd'];
myArray[1] = 'fdsfs'; // error 无法给只读的索引赋值

类类型

与C#或Java里面的接口一样,TypeScript也能够用它来明确的强制一个类去符合它的特征。接口描述了类的公共部分,类在实现一个接口的时候,必须实现接口里面定义的属性和方法。当然,类可以添加出了接口中定义的规范的其他的属性或方法:

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

class Clock implements ClockInterface {
currentDate: Date, // 自己拥有
currentTime: Date; // 实现接口中定义的属性
setTime(d: Date) { // 实现接口中定义的方法
this.currentTime = d;
console.log(d);
}
}

继承继承接口

和类一样,接口可以相互继承,这样可以灵活的将接口分割为可重用的模块。

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

interface Square extends Shape {
sideLength: number;
}

let square = <Square>{
color: 'blue',
sideLength: 0,
};

一个接口可以继承多个接口,创建出多个接口的合成接口。

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

interface PenStroke extends Shape {
penWidth: number;
}

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

let square = <Square>{
color: 'blue',
penWidth: 3,
sideLength: 0,
};

混合类型

前面说到过,接口可以描述JavaScript里丰富的类型,因为JavaScript其动态灵活的特点,我们可以定义一个混合类型使一个对象具有上面提到的多个类型,比如,一个对象可以同时作为函数和对象使用,并且具有额外属性:

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

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

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

接口继承类

当接口继承一个类类型时,它会继承类的成员但不包括实现。接口同时也会继承到类的privateprotected成员。这意味着,当你创建了一个接口继承了一个拥有私有属性或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现。这个特性在当你拥有一个庞大的继承结构时很有用,这个子类除了继承自基类外与基类没有任何关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Control {
private state: any;
select(): void {
console.log('control');
}
}

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

class Image implements SelectableControl { // error 缺少私有属性state
select() {};
}
class TextBox extends Control implements SelectableControl {
select(): void {
console.log('textBox');
}
}
let testBox = new TextBox();
testBox.select();

接口相当于声明了类型,定义了类型。

最后一个接口继承类感觉不是怎么理解。后续还需要留意。

码字辛苦,打赏个咖啡☕️可好?💘