TypeScript系列 --- 接口

274 阅读6分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

TypeScript 的核心原则之一是对值所具有的_结构_进行类型检查

在TypeScript 里,接口的作用就是为对象的具体属性定义类型注解

这里的对象包括普通对象类型,也包括函数,数组等特殊的对象类型

初体验

需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。 然而,有些时候 TypeScript 却并不会这么宽松。

同时类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

接口就好比一个名字,类似于定义了一个新的类型注解(约定俗成,我们推荐接口名使用I开头)

interface IUser {
  name: string
}


function printName(user: IUser) {
  console.log(user.name)
}

const user = {
  name: 'Klaus',
  age: 23
}

printName(user)

接口的属性值和属性值之间可以使用逗号,分号,换行来进行分割

interface NumberOrStringDictionary {
  [index: string]: number | string
  length: number
  name: string
}

// -----------------------------------

interface NumberOrStringDictionary {
  [index: string]: number | string,
  length: number,
  name: string
}

// -------------------------------------

interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number;
  name: string;
}

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。此时可以使用可选属性

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号

interface SquareConfig {
  color?: string;
  width?: number;
}

const square1: SquareConfig = {
  width: 20
}

const square2: SquareConfig = {
  color: 'blue',
  width: 20
}

const square3: SquareConfig = {}

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用readonly来指定只读属性

interface Point {
  readonly x: number;
  readonly y: number;
}

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

ReadonlyArray<T>

TypeScript 具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error
ro.push(5); // error
ro.length = 100; // error
a = ro; // error 不可以将只读数组赋值给普通数组,因为只读数组中的改变元素的方法被移除了
ro = a // success

额外的属性检查

interface Point {
  readonly x: number;
  readonly y: number;
}

// 对象字面量会被特殊对待而且会经过额外属性检查
// 当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时, 会报错
let p1: Point = { x: 10, y: 20, z: 30 }; // error

解决方式1 --- 类型断言

let p1: Point = { x: 10, y: 20, z: 30 } as Point; // 类型断言

解决方式2 --- 字符串索引签名(索引类型)

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any; // key是字符串类型的变量,value是任意类型
}

解决方式3 --- 将这个对象赋值给一个另一个变量

interface Point {
  readonly x: number;
  readonly y: number;
}

const point = { x: 10, y: 20, z: 30 }

// 此时是变量赋值,不是对象字面量赋值,所以不会进行额外属性检测
let p1: Point = point;

函数类型

interface printInfo {
  (name: string, age: number): void;
}

定义好后,我们可以像使用其它接口一样使用这个函数类型的接口。

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。

TypeScript会对函数的参数会逐个进行检查,只要求对应位置上的参数类型是兼容的即可。

interface printInfo {
  (name: string, age: number): void;
}

const printInfo: printInfo = (username: string, age: number) => console.log(username, age)
printInfo('Klaus', 23)

函数在实际调用的时候,函数参数的类型和返回值的类型是可以省略的

TypeScript 的类型系统会推断出参数类型和返回值类型

interface printInfo {
  (name: string, age: number): void;
}

const printInfo: printInfo = (username, age) => console.log(username, age)
printInfo('Klaus', 23)

索引类型

与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如数组,map等

可索引类型具有一个_索引签名_,它描述了对象索引的类型,还有相应的索引返回值类型。

interface strArr {
  [key: number]: string
}

const strArr: strArr =  ['Klaus', 'Alex', 'Jhon']

Typescript 支持两种索引签名:字符串和数字。

两种类型的索引可以同时存在,但是数字索引的返回值必须是字符串索引返回值类型的子类型

因为当使用number来索引时,JavaScript 会将它转换成string然后再去索引对象 ==> arr[1] === arr['1']

class Animal {
  name: string;
}
class Dog extends Animal {
  breed: string;
}

// error
// 因为此时以数值索引的类型会被转换为以字符串作为索引类型的值去取对应的值
// 此时Animal类型的值是无法赋值给Dog类型的值的,因为Dog可能会存在自身特有的属性和方法
interface NotOkay {
  [x: number]: Animal;
  [x: string]: Dog;
}
interface NumberDictionary {
  [index: string]: number;
  length: number; // success => length是number类型
  name: string; // error => `name`的类型与索引类型返回值的类型不匹配
}

// 所以此时可以将索引类型的值修改为联合类型
interface NumberOrStringDictionary {
   [index: string]: number | string;
   length: number;
   name: string; 
}

类类型

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

// 此时Clock必须实现ClockInterface中实现的属性和方法
class Clock implements ClockInterface {
  currentTime: Date = new Date();
  setTime(d: Date) {
    this.currentTime = d;
  }
  constructor(h: number, m: number) {}
}

接口继承

和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

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

interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

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

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型

接口能够描述 JavaScript 里丰富的类型。因为 JavaScript 其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。例如:一个对象可以同时做为函数和对象使用,并带有额外的属性

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

function getCounter(): Counter {
  let counter = function(start: number) {} as Counter;
  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 ImageControl implements SelectableControl {
// Error: Class 'ImageControl' incorrectly implements interface 'SelectableControl'.
//  Types have separate declarations of a private property 'state'.
  private state: any;
  select() {}
}