TypeScript入门五:TypeScript的接口

时间:2023-12-10 14:39:08
  • TypeScript接口的基本使用
  • TypeScript函数类型接口
  • TypeScript可索引类型接口
  • TypeScript类类型接口
  • TypeScript接口与继承

一、TypeScript接口的基本使用

1.1定义TypeScript接口的指令(interface)

接口让我们想到的第一个短语就是(API)。比如日常我们所说的调用某某程序的API一般都是跨应用的接口,如别的网站的或者APP的接口;还有我们每天都是用的编程语言的指令和内置的方法及属性,这些可以叫做编程语言的接口;还有令我们既爱又恨的各种平台、框架、库的庞大接口集,特别是这些东西每一次升级都会产生新的接口或者修改以前的接口,这些接口需要我们付出大量的学习时间。

而学习接口第一要务就是学习接口的语法,这些语法往往定义了各种读写规则,这里抛开接口背后实现的意义不谈,因为学习使用TypeScript接口主要就是学会如何自定义各种接口的规则,至于你将来要在程序中定义什么样的接口、根据什么定义接口规则都是要根据程序本身的性质决定的。

比如按照我们JS的形式给一个函数定义参数接口:

 function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

这里会有类型检查帮助我们检查传入(printLabel)函数的参数是否有指定的属性(label)。然后,来看看TypeScript接口如何定义上面这个参数接口:

interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {//这里指定参数要符合LabelledValue接口规则,参数不许包含一个label属性,且类型为string
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

感觉这两种实现方式没有什么区别,但是对于严格TypeScript的接口来说就没有这门宽松了,TypeScript包含非常丰富的接口规则,定义TypeScript接口最基本的规则就有:可选属性、只读属性、额外属性检查。

1.2可选属性(option bags模式):在可选属性名称定义的后面加上(?)问号。注意使用可选属性后,就不能出现额外的属性了,除非使用后面的额外属性检查规则实现。

 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"} );//可选择传入部分可选属性
let mySquare1 = createSquare({color: "black",width:120} );
// let mySquare2 = createSquare({color: "black",widtg:120} );//不能出现属性名不一致,这同样被识别为额外属性,出现2345错误
// let mySquare3 = createSquare({color: "black",width:120,size:120} );//这里出现了一个额外属性size,会出现2345错误

1.3只读属性:实现只读属性接口时不能不写只读属性,并且也不能出现额外属性,除非使用额外属性检查。

readonly(指定属性只读):在接口类型中指定的属性前面添加readonly修饰符来指定该属性为指定。

ReadonlyArray<T>(定义只读数组):定义只读数组类型结构的接口直接使用ReadonlyArray<T>类型来定义就可以了,不需要使用interface关键字。在官方文档中提到了ReadonlyArray<T>的底层实现就是将数组的可变方法去掉了。

 //readonly
interface Point {
readonly x: number;
readonly y: number;
}
function fun(p:Point):number{
return p.x + p.y;
}
fun({x:1,y:2})
// fun({x:1})//只读属性必须全部写入参数
// fun({x:1,y:2,z:3})//不能添加额外的属性
fun({x:1,y:2,z:3} as Point)//这里使用了额外属性检查就不会报错 //ReadonlyArray<T>定义只读数组
let a:number[] = [1,2,3];
let ro:ReadonlyArray<number> = a;
let ro1:ReadonlyArray<number> = [1,2,3,4];
console.log(ro == ro1);
ro1 = a;
// a = ro; //number[] 类型不等于ReadonlyArray<T> ,这是变量类型那不内容的延伸了,TypeScript对变量的类型有严格的要求
// a = ro1;//同上
a = ro as number[];//可以使用断言重写ro来实现类型转换
// ro[1] = 100;//只读数组不能重写元素值

1.4额外属性检查:在前面的可选属性和只读属性都提到了额外属性检查,额外属性检查顾名思义就是接口中出现了额外的属性,而当在接口中定义可选和只读规则以后就不能出现额外属性,但是有时候在一些实现中可能出现额外属性就可以使用额外属性检查来实现。

额外属性检查就是在实现接口时使用“as”来告诉编译环境,这里允许出现额外属性,只要除额外属性以外的其他属性符合接口的规则就允许实现。

 interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 });//这里不小心把color写成了colour,colour被识别为额外属性了
let mySquare1 = createSquare({ colour: "red", width: 100 } as SquareConfig);//使用额外属性检查就可以解决这类报错

关于额外属性检查,看是可以用来解决一些问题,实际上也确实可以解决一些问题,但是就上面的例子来说,这并不是一个好的结局方案,因为我们期望的时传入color的属性值,但是由于编码失误写错了单词,额外属性帮我们静默了这个错误,也同时让我们的程序偏离了我们期望的方向。关于这样的问题后面有一种索引签名的方式来解决,也就是后面的可索引的类型。

二、TypeScript函数类型接口

在TypeScript函数部分介绍了使用type定义Ts函数类型(详细可以了解官方文档或者这篇博客:TypeScript入门三:TypeScript函数类型),TypeScript接口也可以定义函数的接口类型,定义模式与type定义函数类型非常类似。

 type myAdd = (baseValue: number, increment: number) => number;
interface inAdd {
(baseValue: number, increment: number):number;
}
//根据函数类型和接口声明函数变量
let myAdd1:myAdd = function(x:number,y:number) : number{
return x + y;
}
let myAdd2:inAdd = function(x:number,y:number) : number{
return x - y;
}

函数实现函数接口和函数类的实例化都一样,其都属于函数内部语法,比如可以设置参数默认值;参数名称与函数类或者接口定义的可以不一致,只要参数对应的位置实现的参数类型一致就可以了。

三、TypeScript可索引类型接口

在前面的额外属性检查中就提到了可索引类型接口,并有说过可以通过可索引类型接口解决由于书写错误,并由额外属性导致的错误静默问题。先来了解可索引接口规则是什么?再看看如何解决前面的问题。

3.1数字索引的可索引类型接口:数字可索引类型其实本质上就是定义个数组类型,只要赋值符合该接口的定义就可以直接赋值。

 interface StringArray {
[index: number]: string;
} let myArray: StringArray;
myArray = ["Bob", "Fred"];
let a = ["1","2","3"];
myArray = a;//这里并不会失败,数字可索引类型其实本质上就是定义个数组类型,只要赋值符合该接口的定义就可以直接赋值
let myStr: string = myArray[0];

3.2字符串可索引类型接口:字符串可索引类型其本质就是定义对象类型,只要赋值符合该接口定义就可以直接赋值。

 interface StringObject {
[key:string]:string
}
let myObj : StringObject = {
'name':'他乡踏雪',
'professional':'Web front end'
}

3.3字符串和数字两种索引签名组合使用:

这种复杂的接口应用在官方文档中只给出了一个引导性的示例,官方使用了文字描述使用方法,而示例只给出了错误使用提示,并没有给完整的正确示例,估计这让很多初学者比较头疼,我这两天到各个教程中寻找完整的正确示例,但都没有找到,最后通过n次测试终于实现了两种索引组合使用的正确示例。直接上代码,在代码的注释中解析这种接口实现的规则:

 class Animal {
//定义类作为接口索引指向的值时:
// 类内部的成员必须初始化赋值或者基于构造函数实例化赋值
// 这里我采用了构造函数实例化赋值
name: string;
constructor(nameStr:string){
this.name = nameStr;
}
}
class Dog extends Animal {
breed: string;
constructor(breedStr:string,nameStr:string){
super(nameStr);
this.breed = breedStr;
}
}
// 数字和字符串索引签名组合使用时:
// 数字索引赋值类型必须是字符串索引赋值类型的子类型
// 这个示例中数字索引赋值的Dog类型就是字符串索引赋值的Animal的子类型 //接口中定义索引时索引签名的类型是关键,
// 索引定义的标识名称(示例中的‘x’)并不重要,
// 可以使用任何合法字符,我没有测试过特殊字符,
// 但实际上它是无意意义的,比如示例中两个x可以替换成x和y照样不影响接口实现
interface animals {
[x: number]: Dog;
[x: string]: Animal;
}
//基于数字与字符串索引组合的接口实现,
// 使用{}定义成对象类型
// 不能使用[]只实现数字索引的值
// 但可以使用{}只实现字符串索引的值
let myAnimals : animals = {
1:new Dog('Dog1','animal1'),
'2':new Dog('Dog2','animal2'),
'animal1':new Animal('animal1'),
'animal2':new Animal('animal2')
}
// let myAnimals1 : animals = [new Dog('Dog1','animal1')];//报错
let myAnimals2 : animals = { //这里不报错
'animal1':new Animal('animal1'),
'animal2':new Animal('animal2')
} console.log(myAnimals);

在可索引类型类型接口中照样可以使用只读属性规则:

 interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

前面说要基于可索引类型接口展示一个解决额外属性产生的潜在问题,写到这里我感觉好像没有多大必要了,如果你真的搞懂了可索引类型接口规则,应该很轻松想到它可以如何实现,还有考虑到篇幅问题,就不在这里展示了,如果有需要可以在评论区留言。

四、TypeScript类类型接口

TypeScript类类型接口与抽象类非常类型,都是用来定义类的必须结构,但是类类型接口与抽象类还是有些区别,抽象类中可以实现具体的细节,但是在类类型接口中不能实现具体的细节。比如在抽象类中可以直接实现方法的具体内容,但是在类类型接口只能声明一个方法的类型(包括名称、参数类型、返回值类型),但不能在类类型接口中直接实现函数体的具体内容。

 interface ClockInterface {
currentTime: Date;//指定强制实现的公共成员,在接口实现中必须初始化或者在构造其中赋值
setTime(d: Date):void;//指定强制实现的函数,在接口实现中必须实现,之一这里定义时需要有指定的返回值类型,void表示没有任何类型,也就是不返任何类型的回值 } class Clock implements ClockInterface {
currentTime: Date;
//↑上面这里第一种是想方法:实现接口强制约定要实现的公共成员currentTime,在构造器中赋值实现
//↓ 下面这里第二种实现方法: 实现接口强制约定要实现的公共成员currentTime,初始化值的方式实现
// currentTime: Date = new Date(); setTime(d: Date) {//函数默认不返回任何类型的值,默认值为undefined,但它不是undefined类型
this.currentTime = d;
}
constructor(h: number, m: number) {
this.currentTime = new Date();
}
}

关于类类型接口与抽象类的区别还有:

增加其他内容与否:派生类实现抽象类时不能随意增加其他内容,一切根据抽象类定义的结构实现,但是在类类型接口的具体实现中可以增加类类型接口中没有定义的内容。

公共成员与私有成员:抽象类中可以实现公共成员,也可以实现私有成员。但是,类类型接口中非静态成员都是公共公共成员,而且并不需要public修饰,自动被默认为公共成员。

静态成员在抽象类和类类型接口中都可以直接使用static修饰符来定义。

上面都是关于修饰符的相关内容,还有一个问题就是类类接口中不能直接定义构造函数的相关构造结构,在官方文档中描述为类的静态部分,也就常说的constructor函数。而当对一个类类型接口做具体实现时,只会对类类型的实体部分进行检查,而不会对静态部分检查。

而且定义类类型接口时并不能直接使用constructor在类类型接口中定义相关结构,IDE会直接提示错误信息,官方给出的解决方案时使用构造器签名的方式来实现。具体的实现方案就是定义一个构造器签名的类类型接口、定义一个实体部分的类类型接口、然后在根据这两个类类接口的结构的具体实现写在一个类中,这个类的implements指向实体部分的类类型接口、最后定一个实例化工具方法,将方法的第一个参数类型设定为构造器签名的类类型接口类型,实现构造需要的参数跟着写后面,然后指定这个方法的返回类型为实体部分的类类型接口类型,方法内直接返回第一个参数的实例化,实例化时传入对应需要的实例化参数。

具体见下面这个官方文档中的示例,对照下面的注释应该很清晰了:

 interface ClockConstructor {//定义构造器签名的类类型接口:作为类类型接口的构造函数的结构
new (hour: number, minute: number): ClockInterface;//构造函数的方法值设定为类类型主体类型
}
interface ClockInterface {//定义类类型接口的实体
tick():void;
}
//定义一个类类型接口实现的实例化工具方法,设置参数有:将类类型实现的类的类型设置为构造签名的类类型接口类型,以及构造器需要的参数
// 返回值类型设置为类类型接口实体类型
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);//内部直接通过类类型的具体实现类实例化并返回即可
}
//类类型接口的具体实现,implements指向类类型接口的实体类型
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }//内部构造函数根据定义的构造器签名的类类型接口结构实现
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
//将类类型接口的具体实现类作为参数以及构造函数需要的参数一并传入,实例化类类型接口具体实现的类
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

五、TypeScript接口与继承

接口继承与类继承没什么差异,通过继承可以将被继承的接口内容复制过来,通过继承可以更灵活的将接口分割重用,这里我就直接复制官方文档的一个示例代码展示了,因为从接口语法角度来说非常简单,但是如果要使用上面类类型接口那样的复杂接口来继承另当别论。

 interface Shape {
color: string;
} interface PenStroke {
penWidth: number;
} interface Square extends Shape, PenStroke {
sideLength: number;
} let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

接口实现混合类型(函数类型与函数对象的静态方法接口):

在前面已经介绍了关于函数类型接口的实现,但是仅仅只能实现一个单独的函数,在JS中函数本身就是一个对象,在函数对象上实现静态的工具方法是比较常见的现象,TypeScript定义函数类型接口并一同定义一些工具方法也可以实现,这有点像是继承加手动解构的模式,官方文档中的示例也是比较清晰易懂的,还是添加一些注释的方式解析吧。

 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;

接口继承类:

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。

在实现这类接口的时候必须要同时实现继承该接口继承的类,或者继承该接口继承的类的子类,比如示例:

 class Control {
private state: any;
} interface SelectableControl extends Control {
select(): void;
}
//实现接口同时实现继承接口继承的类
class Button extends Control implements SelectableControl {
select() { }
} class TextBox extends Control {
select() { }
}
//实现接口同时实现继承接口继承的类的子类
class TextBox1 extends TextBox implements SelectableControl{ }