前两天跟字节跳动的大佬交流了一下,了解他们项目主要用的什么技术栈,他简略地告诉我是react跟ts,我直呼一个“好家伙”,果然大厂基本都用这两个。之后又具体问了一下ts用得比较多的知识点是哪一块(因为我个人觉得js跟ts的差别并不是很大,只是ts多出了一些扩展功能),他跟我说用得相对较多的是interface,也就是接口。于是我决定单独对ts的接口(interface)进行介绍,加深印象。之前有总结过ts的基础知识,其中有为ts与js做了简单对比,不了解的可以移步前往阅读。
TypeScript 与 JavaScript 的区别(TypeScript万字基础入门,了解TS,看这一篇就够了)_前端不释卷leo的博客-CSDN博客TypeScript 是 JavaScript 的一个超集,支持 ECMAScript 6 标准(ES6 教程)。TypeScript 由微软开发的自由和开源的编程语言。TypeScript 设计目标是开发大型应用,它可以编译成纯 JavaScript,编译出来的 JavaScript 可以运行在任何浏览器上。TypeScript 是一种给 JavaScript 添加特性的语言扩展。增加的功能包括:类型批注和编译时类型检查类型推断类型擦除接口枚举Mixin泛型编程名字空间https://blog.csdn.net/qq_41809113/article/details/121107014?spm=1001.2014.3001.5502准备工作:先使用vue-cli脚手架新建一个vue项目,选择自定义项目配置,注意一定勾选TypeScript,其他项可以自行按实际情况选择。
项目新建成功后,进入项目文件夹,执行 npm run serve 启动项目。
项目目录结构如下
这时我们的项目就是一个支持ts的vue项目。
当然,这不是咱们的重点,只为搭个运行环境,方便咱们执行代码,演示实例,本章的重点在于ts中的接口(interface)。
进入重点!!!
TypeScript 接口
TypeScript的核心原则之一是对值所具有的shape进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法。简单来说,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
接口定义
interface interface_name {
}
实例
以下实例中,我们定义了一个接口 IPerson,接着定义了一个变量 student,它的类型是 IPerson。student 实现了接口 IPerson 的属性和方法。
interface IPerson {
firstName:string,
lastName:string,
sayHi: ()=>string
}
var student:IPerson = {
firstName:"Leo",
lastName:"Gao",
sayHi: ():string =>{return "Hi there"}
}
console.log("Student 对象 ")
console.log(student.firstName)
console.log(student.lastName)
console.log(student.sayHi())
控制台打印结果如下
现在,我们的这个接口对其中的一些属性类型做了约束,如果类型为这个接口的类或者对象里面的属性不存在或者类型与接口定义的不一致,就会编译报错,如:
var student:IPerson = {
firstName:18, //将该属性的类型变成number,与接口定义时不一致
lastName:"Gao",
sayHi: ():string =>{return "Hi there"}
}
把Student对象的firstName赋值为18,即此时它的类型变成了number,这时候编译报错:
属性不一致也会编译报错:
var student:IPerson = {
secondName:"leo", //添加接口定义中不存在的属性
lastName:"Gao",
sayHi: ():string =>{return "Hi there"}
}
去掉某个属性也会编译报错:
var student:IPerson = { //去掉firstName属性
lastName:"Gao",
sayHi: ():string =>{return "Hi there"}
}
以上均验证了开篇对接口的介绍时说到,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。试想一下,如果多人同时开发,这样规范化的约束(编译不能报错),即使代码辗转多人之手也不会显得杂乱无章,相反会使得代码定义、结构、逻辑清晰。
另外一个简单的例子:
现在,在没有使用接口之前,我们有这么个例子:
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label)
}
let myObj = { size: 10, label: "Hello TypeScript" }
printLabel(myObj)
类型检查器会查看printLabel的调用。printLabel有一个参数,并要求这个对象参数有一个名为label类型为string的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。
这时候,我们使用接口描述,重写上面的例子,约束为必须包含一个label属性且类型为string。
interface LabelledValue {
label: string
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label)
}
let myObj = {size: 10, label: "Hello TypeScript"}
printLabel(myObj)
LabelledValue接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个label属性且类型为string的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给printLabel的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。
假如将上面的传入对象修改:
let myObj = {size: 10, greet: "Hello TypeScript"}
printLabel(myObj)
此时编译必然会报错,因为已经不满足接口的必要条件(label:string),即约束条件。
在对接口有一些基本了解之后,接着对接口进行扩展。
联合类型和接口
以下实例演示了如何在接口中使用联合类型(如不熟悉联合类型,请前往ts基础篇了解):
interface RunOptions {
program:string;
commandline:string[]|string|(()=>string);
}
// commandline 是字符串
var options:RunOptions = {program:"test1",commandline:"Hello"};
console.log(options.commandline)
// commandline 是字符串数组
options = {program:"test1",commandline:["Hello","World"]};
console.log(options.commandline[0]);
console.log(options.commandline[1]);
// commandline 是一个函数表达式
options = {program:"test1",commandline:()=>{return "**Hello World**";}};
var fn:any = options.commandline;
console.log(fn());
联合类型定义属性可以让我们在使用接口描述时类型可以动态变化,如上面的commandline可以是三种类型,字符串、字符串数组、函数表达式均不会编译错误。
这里不得不提一个与其相似的知识点,那就是“可选属性”。
接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。
下面是应用了“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"});
console.log(mySquare)
此时,我们在调用函数时,传{ }、{color: "black"}、{color: "black", area: "50"} 都是允许的,因为两个属性均为可选属性,非必须。
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?
符号。
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。 比如,我们故意将 createSquare 里的 color 属性名拼错成 culor,就会得到一个错误提示:
接口和数组
接口中我们可以将数组的索引值和元素设置为不同类型,索引值可以是数字或字符串。
interface namelist {
[index:number]:string
}
var list2:namelist = ["John",1,"Bran"] // 错误元素 1 不是 string 类型
interface ages {
[index:string]:number
}
var agelist:ages
agelist["John"] = 15 // 正确
agelist[2] = "nine" // 错误
这里我们可以结合上面的例子,上面例子中,如果我们将传入的参数color改成culor,可想而知编译会报错,因为必要属性不一致:
let mySquare = createSquare({culor: "black"})
console.log(mySquare)
这里知道接口中可以将数组的索引值和元素设置为不同类型,那么我们可以引入一个“额外属性”,添加一个字符串索引签名,如
interface SquareConfig {
color?: string
width?: number
[propName: string]: any //添加一个字符串索引签名,类型为any
}
表示的是SquareConfig可以有任意数量的属性,并且只要它们不是color和width,那么就无所谓它们的类型是什么。此时编译便不报错了。
接口继承接口
单继承实例
interface Person {
age:number
}
interface Musician extends Person { //通过继承将Person的属性继承过来
instrument:string
}
var drummer = <Musician>{}
drummer.age = 27
drummer.instrument = "Drums" //通过继承获得instrument属性
console.log("年龄: "+drummer.age)
console.log("喜欢的乐器: "+drummer.instrument)
多继承实例
interface IParent1 {
v1:number
}
interface IParent2 {
v2:number
}
interface Child extends IParent1, IParent2 { } //多继承,获取所有被继承接口的属性
var Iobj:Child = { v1:12, v2:23}
console.log("value 1: "+Iobj.v1+" value 2: "+Iobj.v2)
接口继承类
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement),例如:
class Control {
private state: any;
}
interface SelectableControl extends Control { //继承一个拥有私有属性的类
select(): void;
}
class Button extends Control implements SelectableControl { //Button为Control的子类
select() { }
}
class TextBox extends Control { //TextBox为Control的子类
select() { }
}
// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl { //Image不是Control的子类,因此不存在state属性
select() { }
}
class Location {
}
因为Image不是Control的子类,因此不存在state的属性,但是SelectableControl接口要求必须有state属性,因此编译报错。这是很有用的,当你有一个很深层次的继承,但是只想你的代码只是针对拥有特定属性的子类起作用的时候。子类除了继承自基类外与基类没有任何联系。
在上面的例子里,SelectableControl包含了Control的所有成员,包括私有成员state。 因为state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口。 因为只有Control的子类才能够拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的。
在Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。 实际上,SelectableControl就像Control一样,并拥有一个select方法。Button和TextBox类是SelectableControl的子类(因为它们都继承自Control并有select方法),但Image和Location类并不是这样的。
总结:万变不离其宗!