likes
comments
collection
share

接口interface:掌握Typescript的关键之一

作者站长头像
站长
· 阅读数 19

什么是接口

在用 ts 开发项目时,使用最多的必然就是接口 interface 了。那什么是接口呢?

接口是用来描述一个具体的对象、函数或者类的类型,它的作用就是为这些类型命名,以及为你的代码提供代码定义契约

它有时被称为"鸭子式辨型法",如果一个东西能长的像鸭子, 像鸭子一样游泳,一样叫,那么就可以认为这个东西就是一个鸭子。通过一个例子来说明:

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

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

getStudentInfo(student); // 18

getStudentInfo接收一个参数student,它的类型是{ age: number },我们这里传入的数据多了个属性name,但是 ts 进行类型检测的时候它并不会提示错误。

这是因为我们传入的对象当中包含age属性,并且类型为number,编译器只会检查那些必须的属性是否存在,并且其类型是否匹配。

这种检测方式称为鸭子辨型法

上面代码中function getStudentInfo(student: { age: number }),这样写很不优雅,我们用 interface 定义一个类型,如下:

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

接口描述不同类型

1. 描述一个具体的对象

interface IOption {
  label: string;
  value: number;
}

const obj: IOption = {
  label: 'key',
  value: 1,
}

2. 描述函数类型

interface UserInfoFn {
  (age: number, name: string) : string
}

const fn: UserInfoFn = function (age: number, name: string) {
  return `${name}今年${age}岁了`
}

还可以描述函数的重载:

// 定义一个函数重载
interface UserInfoFn {
  (age: number): string
  (age: number, name: string): string
}

function getAdd(): UserInfoFn {
  function add(age: number): string
  function add(age: number, name: string): string
  // 这里的name必须写成name?
  function add(age: number, name?: string): string {
    if (name) {
      return name + age
    } else {
      return age + ''
    }
  }
  return add
}

3. 描述类,使用 implements 实现接口

interface IAnimal {
  name: string;
  age: number;
}

class Panda implements IAnimal {
  name = "mike";
  age = 18;
}

4. 描述构造函数

interface People {
  run: () => void;
}

interface PeopleConstructor {
  new (name: string): People;
}

class Student implements People {
  constructor(name: string) {}
  run() {
    console.log("run");
  }
}

function createPeople(construction: PeopleConstructor, name: string): People {
  return new construction(name);
}

const stu = createPeople(Student, "小明");

可索引的接口

当你不确定接口里面属性是什么时,可以使用可索引接口。它描述了对象索引的类型,还有相应的索引返回值类型。

interface StringArray {
  [index: number]: string
}
let nameArray: StringArray = ['mike', 'jack'] // ok
let numArray: StringArray = [1, 2] // error

StringArray定义了一个数字类索引,即 key 只能是数字,其实就是一个数组。

Typescript 支持 numberstring 两种索引签名。下面是字符串类索引。

interface Obj1 {
  [key: string]: string
}

let s1: Obj1 = {
  name: 'xiaom',
  female: 'nan'
}

两种索引可以同时使用,但是,由于使用 number 索引时,js 默认会将其转化为 string 再去索引对象,所以,数字索引的返回值必须是字符串索引返回值类型的子类型。

class Animal {
  name: string = 'xiaom'
}
class Dog extends Animal {
  breed: string = 'eat'
}

interface NotOkay {
  [x: number]: Animal // 报错
  [x: string]: Dog
}

interface Okay {
  [x: number]: Dog // 正常
  [x: string]: Animal
}

还需要注意一点,字符串的索引签名会让所有属性与其返回值类型相匹配。

interface St {
  quantity: 20; // error
  visible: true; // error
  name: 'jack'; // ok
  10: 0; // error
  11: ''; // ok
  [x: string]: string;
}

接口的属性

1. 可选属性

在可选属性名字定义的后面加一个?符号,表示该属性"可有可无":

interface Persion {
  name: string
  age?: number
}

const p: Persion = {
   name: 'xiao'
}

2. 只读属性

在只读属性名字定义的前面加上关键词 readonly,表示该属性赋值后不可再改变:

interface IUser {
  readonly name: string
  readonly age: number
}
let myUser: IUser = {
  name: 'mike',
  age: 18
}
myUser.age = 20 // error

对象字面量的错误

有一种错误需要注意下,看如下的例子中的注释:

interface Persion {
  name: string
  age?: number
}

function getPersonName(person: Persion) {
  return person.name
}

// person1里面多了一个sex属性,作为参数传递进去是不会报错的,只要参数里面有name这个属性即可
const person1 = {
  name: 'jack',
  sex: 'female'
}

getPersonName(person1)

// 如果这么写就会报错,这是因为ts会对字面量进行强校验,不只是要求有name属性,也不能有多余的属性
getPersonName({
  name: 'jack',
  sex: 'female'
})

接口继承

和类一样,接口也可以相互继承

interface People {
  name: string;
}

interface Chi extends People {
  language: string
}

interface Usa extends People {
  skinColor: string
}

// 也可以继承多个
interface HalfBlood extends Chi, Usa{ 
  weight: number
}

let Jessie:HalfBlood = {
  name: 'Jessie',
  language: 'Chinese',
  skinColor: 'yellow',
  weight: 50,
}

另外,需要注意一点:同名的接口会把各自的成员合并到一起

interface people {
  name: string
}
interface people {
  age: number
}

const p1: people = {
  name: 'mike',
  age: 18
}

如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错。

interface people {
  height: string
}
interface people {
  height: number // error
}

接口与typeclass

type

type 是 TypeScript 中用于定义类型别名、联合类型、交叉类型等复杂类型的声明方式。

interfacetype 在编译后的 JavaScript 代码中被移除,因为它们仅在编译阶段用于类型检查。换句话说,type 不需要运行时信息。

相比之下,class 定义的类型信息会保留在编译后的代码中,因为它们包含实际的属性和方法实现,这些信息在运行时是必需的。

1. 类型别名(Type Aliases)

类型别名是给一个类型起一个新名字。例如:

type String1 = string;

2. 联合类型(Union Types)

联合类型表示一个值可以是多个类型中的一种。例如:

type StringOrNumber = string | number;

3. 交叉类型(Intersection Types)

交叉类型表示一个值必须满足多个类型的要求。例如:

type Name = { name: string };
type Age = { age: number };
type Person = Name & Age;
const t1: Person = {
  name: 'xiaom',
  age: 123
}

class

class 是一种定义类型和实现的方式。它既包含类型信息,也包含实际的属性和方法实现。接口只定义了类型,并不包括实现。

class 可以通过关键字 extends 实现类继承,还可以通过关键字 implements 实现接口实现。这使得 class 成为创建具有多层次结构和行为的对象的理想选择。

class User {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  sayHello(): void {
    console.log(`Hello, my name is ${this.name}`);
  }
}

class Employee extends User {
  role: string;

  constructor(name: string, age: number, role: string) {
    super(name, age);
    this.role = role;
  }
}

使用场景

  1. type 适用于定义类型别名、联合类型、交叉类型等。

  2. interface 主要用于定义对象的类型和形状,支持继承和实现。

  3. class 既包含类型信息,也包含实际的属性和方法实现。

虽然 typeinterface 在很多场景下可以互换使用,但它们在某些特定场景下有着各自的优势。type 更适用于组合不同类型,如联合类型、交叉类型等,而 interface 更适用于定义对象的形状,特别是在面向对象编程中。class 则提供了完整的类型定义和实现,可以在运行时进行实例化和操作。

在实践中,我们应该根据实际需求和场景选择合适的类型声明方式。例如,在定义一个复杂的对象类型时,可以使用 interface;在组合不同类型时,可以使用 type;在创建具有行为的对象时,可以使用 class