一文搞懂 typescript 当中的接口

1,043 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 3 天,点击查看活动详情

接口介绍

Typescript 和核心原则之一是对值所具有的结果进行类型检查。它有时被称为“鸭子式辨型法”。当他具有鸭子的形状,鸭子的叫声,那么它就是一只鸭子。在 typescript 当中,接口的作用就是为这些类型命名和为你的代码提供第三方的代码定义契约。 下面是一段代码,观察接口的工作

function getStudentInfo(student: { age: number }) {
  console.log(student.age);
}

const student = { age: 18, name: "张三" };

getStudentInfo(student); //18

上面的getStudentInfo函数接收一个参数studentstudent的类型是{ age: number },其中包含了一个age属性类型为number,但是需要注意,我们在传入数据的时候,会传入多个属性,但是 typescript 进行类型检测的时候它并不会提示错误,因为我们传入的对象当中包含age属性,并且类型为number,编译器只会检查那些必须的属性是否存在,并且其类型是否匹配。这种检测方式也称为鸭子辨型法。

接口定义

typescript 当中我们定义接口使用关键字interface 来定义。 上面的代码我们使用接口来实现,看看效果如何:

//定义一个学生接口
interface IStudent {
  //   学生学号
  sid: number;
  //   学生姓名
  name: string;
  //   学生年龄
  age: number;
  //   学生性别
  sex: string;
  //   学生出生年月日
  birth: string;
}

// 使用接口来定义传入参数的类型
function getStudentInfo(student: IStudent) {
  console.log(student.age);
}
/*
下面的代码会报类型错误
Error 类型“{ age: number; name: string; }”的参数不能赋给类型“IStudent”的参数。
Error 类型“{ age: number; name: string; }”缺少类型“IStudent”中的以下属性: sid, sex, birth
*/
// const student = { age: 18, name: "张三" };

//使用接口类定义对象的类型
const student: IStudent = {
  age: 18,
  name: "张三",
  sid: 111,
  sex: "男",
  birth: "2021年4月18日",
};

getStudentInfo(student); //18

上面的代码当中,如果函数getStudentInfo的参数类型变成IStudent,然后继续传入上面的对象的话,会报类型错误,因为我们传入的student对象和函数接收的参数类型不一致,所以就会报类型错误。后来改成没注释的代码,就可以正常执行,因为现在的代码传入的数据和接收的数据类型是一样的,属性都实现了。接口就是对一个数据的描述,或者说是约定,当你不遵守约定,那么就无法正常的实现代码。

可选属性

接口当中的属性并非全部是必须使用的,我们也可以使用?来定义可选属性。可选属性顾名思义就是可以有也可以没有。

//定义一个学生接口
interface IStudent {
  //   学生学号
  sid: number;
  //   学生姓名
  name: string;
  //   学生年龄
  age: number;
  //   学生性别
  sex?: string;
  //   学生出生年月日
  birth?: string;
}

// 使用接口来定义传入参数的类型
function getStudentInfo(student: IStudent) {
  console.log(student.age);
}

//使用接口类定义对象的类型
const student: IStudent = {
  age: 18,
  name: "张三",
  sid: 111,
};

getStudentInfo(student); //18

上面的代码当中,我们这次并没有传入birthsex属性,但是 ts 的类型检测却并没有提示语法错误,我们还注意到,上面的接口当中,birthsex属性后面都添加了一个?,这代表着两个属性都是可选的属性。

只读属性

接口当中如果我们希望某个属性只读,无法进行更改,可以使用关键字readonly来进行修饰,比如说,我们把上面的代码修改一下:

//定义一个学生接口
interface IStudent {
  readonly sid: number;
  name: string;
  age: number;
}

// 使用接口来定义传入参数的类型
function getStudentInfo(student: IStudent) {
  console.log(student.age);
  //Error 无法分配到 "sid" ,因为它是只读属性
  //student.sid = 111;
}

//使用接口类定义对象的类型
const student: IStudent = {
  age: 18,
  name: "张三",
  sid: 111,
};

getStudentInfo(student); //18

上面的代码当中,我们给属性sid前面使用了readonly来修饰,当我们想要在函数getStudentInfo当中修改sid的时候,TS 的语法检测会给我们提示无法分配到 "sid" ,因为它是只读属性。你可以给sid赋一个初始值,但是之后就不能再修改了。TypeScript 具有ReadonlyArray<T>类型,它和Array<T>类似,但是,它创建的数组是可读的,创建之后,对数组值的所有改变将不被允许。并且当你把整个数组都赋值给一个普通数组的操作也是不被允许的,除非使用类型断言as

函数类型

接口不仅可以描述 javasript 当中带有属性的普通对象,还可以对函数类型进行检测:

interface ISum {
  (a: number, b: number): number;
}

上面是一个接口,接口当中定义了一个的函数,函数接收两个参数ab,类型都是number,接下来我们来实现这个接口:

let sum: ISum = function (a: number, b: number) {
  return a + b;
};

上面的代码当中,我们定义了一个变量sum类型为ISum,然后我们将一个同类型函数赋值给这个变量。这样就实现了接口。对于函数类型来说,函数的参数不需要和接口定义的的参数名字完全一样,但是参数类型需要一致。

可索引的类型

和使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引来获得的类型”,比如arr[10]items[index]等。可索引对象具有一个索引签名,它描述了对象的索引的类型,还有相对应索引返回值的类型。

interface Iitems {
  [propName: number]: string;
}

上面的接口Iitems表示当我们使用number去索引Iitems会得到一个string类型的值。我们来实现一下这个接口

interface Iitems {
  [propName: number]: string;
}

let items: Iitems;

for (let i = 0; i < 10; i++) {
  items[i] = i + "";
}

console.log(items[1]); // '1'

上面的代码当中,定义变量items类型是Iitems。然后我们通过 for 循环给items进行赋值。随后我们通过数字1来索引items当中所对应的值。 Typescript 支持两个索引签名:字符串和数字,可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串。因为使用number类型来索引时,JavaScript 会将他转换成string类型,然后再去索引对象。也就是说用100去索引等于使用"100"去索引。

类类型

接口的类类型和 Java、C#里面的作用基本一样,TS 当中也可以通过接口去约束一个类当时有哪些方法和属性。

interface ICar {
  color: string;
  size: number;
}

class Car implements ICar {
  public color: string;
  public size: number;
}

上面的代码中,定义了一个接口ICar,接口描述了两个属性colorsize。之后,我们定义了一个类Car并且通过implements实现了接口ICar。在类中,我们实现了接口当中的两个属性colorsize。当然,我们不仅可以在接口当中描述属性,我们还可以在接口当中描述方法。

interface ICar {
  color: string;
  size: number;
  drive();
}

class Car implements ICar {
  public color: string;
  public size: number;
  constructor() {}

  drive() {}
}

上面的代码当中,我们给ICar接口添加了方法描述drive。随后我们在类当中也实现了该方法,如果我们不实现该方法,会报错Car错误实现接口ICar, 类型 Car 中缺少属性 drive,但类型 ICar 中需要该属性。

接口继承类

接口也可以继承类,但是继承了类的接口,只能被当前类实现或者被当前类的子类实现。

class Appliance {
  private state: number;
  Company() {}
}

interface ICar extends Appliance {
  color: string;
  size: number;
  drive();
}

class Car extends Appliance implements ICar {
  public color: string;
  public size: number;
  constructor() {
    super();
    this.Company();
  }

  drive() {}
}

上面的代码当中,我们新定义了一个类Appliance,我们在类中新定义了一个方法Company,和私有属性state,然后我们通过接口ICar继承了这个类,现在,如果我们要实现ICar类,那么我们就只能使用Appliance类来实现或者其子类实现。其他的类将无法实现这个接口。因为只有Appliance的子类才能够拥有一个声明于Appliance的私有成员state,这对私有成员的兼容性是必需的。