【TypeScript】接口

2021/09/09 09:36:06

摘要

  1. 使用 extends 关键字实现接口继承,implements 关键字让类实现接口。
  2. 接口只能描述对象的外形
  3. 可以用接口描述对象、函数、数组(可索引对象)和类。
  4. 可以在一个接口里定义多种类型的描述。
  5. 用接口描述函数参数时会有额外的检测机制,可以用类型断言等方法绕过额外的检测。

类型描述

描述对象

接口可以描述 js 中对象拥有的各种外形

interface LabelValue {
    label: string,      // 必选属性
    value?: number,     // 可选属性
    readonly Y: number, // 只读属性,只能在对象刚刚创建的时候修改其值
}

只读属性

只读属性只能在对象刚刚创建的时候修改其值

用接口定义函数参数时的额外属性检测

对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。

下面的例子里接口 LabelValue 定义了一个必填属性 label ,函数 funA 参数定义为 LabelValue 类型,这时如果给该函数传递一个 LabelValue 类型定义中没有的属性时会报错。

interface LabelValue {
  label: string;
}
function funA(config: LabelValue) {}
funA({ label: "", a: 1 });
  • 解决方案一:类型断言
funA({ label:'', a: 1, } as LabelValue)
  • 解决方案二:添加字符串索引签名

注意 propName 不是固定的,改为任何字符串均可。

LabelValue 可以有任意数量的属性,并且只要它们不是 label ,那么就无所谓它们的类型是什么。

interface LabelValue {
  label: string;
  [propName: string]: any;
}
  • 解决方案三:赋值给其他变量

将原本的参数赋值给变量 params 传入函数时不会受到额外的检测。

let params = {
  label: "",
  a: 1,
};
funA(params);

描述函数

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

函数的参数名不需要与接口里定义的名字相匹配。函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。

interface SearchFunc {
  (source: string): boolean;
}
let mySearch: SearchFunc = () => true;
mySearch("");

描述可索引的类型

TypeScript 支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。 也就是说用 100(一个 number)去索引等同于使用"100"(一个 string)去索引,因此两者需要保持一致。

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];
  • 数字索引的返回值必须是字符串索引返回值类型的子类型
class Animal {
  name: string;
}
class Dog extends Animal {
  breed: string;
}

// 错误:数字索引类型“Animal”不能赋给字符串索引类型“Dog”
interface NotOkay {
  [x: number]: Animal;
  [x: string]: Dog;
}

// 正确
interface NotOkay2 {
  [x: number]: Dog;
  [x: string]: Animal;
}

将索引签名设置为只读阻止赋值

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

描述类

使用 implements 关键字让类实现接口

interface ClockInterface {
  currentTime: Date; // 定义属性
  setTime(date: Date): Date; // 定义方法
}

class Clock implements ClockInterface {
  currentTime: Date;
  setTime(date) {
    return date;
  }
}

类的静态与实例部分,定义类的 constructor

当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor 存在于类的静态部分,所以不在检查的范围内。

要想定义 constructor ,需要使用一些技巧

interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
    tick();
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute);
}

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);

接口继承

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape {
    sideLength: number;
}

interface Square2 extends Shape, Square {    // 继承多个接口
    sideLength2: number;
}

let square = <Square2>{};

接口合并

接口合并从根本上说是把双方的成员放到一个同名的接口里。

接口的非函数的成员应该是唯一的。如果它们不是唯一的,那么它们必须是相同的类型,否则编译器会报错。

interface Box {
  height: number;
  width: number;
}

interface Box {
  scale: number;
}

let box: Box = { height: 5, width: 6, scale: 10 };

对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。 后面的接口具有更高的优先级(其重载会在靠前位置)。

如果签名里有一个参数的类型是单一的字符串字面量(比如,不是字符串字面量的联合类型),那么它将会被提升到重载列表的最顶端(依然遵循后来者居上)。

interface Document {
  createElement(tagName: any): Element;
}
interface Document {
  createElement(tagName: "div"): HTMLDivElement;
  createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
  createElement(tagName: string): HTMLElement;
  createElement(tagName: "canvas"): HTMLCanvasElement;
}

合并后的 Document 将会像下面这样:

interface Document {
  createElement(tagName: "canvas"): HTMLCanvasElement;
  createElement(tagName: "div"): HTMLDivElement;
  createElement(tagName: "span"): HTMLSpanElement;
  createElement(tagName: string): HTMLElement;
  createElement(tagName: any): Element;
}

混合类型

一个对象可以同时做为函数和对象使用,并带有额外的属性。

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();