1

#### 你要知道的
TypeScript的核心原则之一是对值所具有的结构进行类型检查。接口的作用就是为类型命名和为代码或第三方代码定义契约或者约束。
#### 接口
什么时候该使用接口呢,先看下面一个示例。

function printLabel(labelledObj: { label: string }) {
   console.log(labelledObj.label);
}

函数printLabel有一个参数,并且这个参数对象要有一个类型为string名为label的属性。我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。但有些时候ts没有那么宽松,就会出现问题。
如果这个约束使用接口,如下:

interface LabelledValue {
    label: string
}

function printLabel(labelledObj: LabelledValue) {
    console.log(labelledObj.label);
}
let myObj = { size: 10, label: 'Size 10 Object' };
printLabel(myObj);

这个接口描述了传入的参数对象要满足的条件,只要这个参数对象含有一个名为label,类型为string的属性即可。
区别于其他语言,这里只能说是对象的类型格式由接口来约束,不能说是对象实现了借口。像上面说的那样,只要满足条件即可,顺序也不在检查范围内。

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。
此时就可以使用可选属性,带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号。

interface Square {
    color: string
    area: number
}

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

function createSquare(config: SquareConfig): Square {
    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' })

SquareConfig接口在描述createSquare函数的参数约束时,允许参数对象含有color和width属性,但不是必须的。

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。
接口只读属性:

interface Point {
    readonly x: number
    readonly y: number
}
// x,y不能改变
let p1: Point = { x: 10, y: 20 }
// p1.x = 5 // error

泛型只读数组:

let a: number[] = [1, 2, 3, 4]
let ro: ReadonlyArray<number> = a
// ro不能修改,并且
// a = ro  // error 泛型只读数组不能赋值给普通数组
// 如需要赋值则使用类型断言
a = ro as number[]

另外const可以声明只读变量。也就是说如果涉及只读属性就用readonly,涉及变量就用const。

额外的属性检查

直接看示例:

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

function createSquare(config: SquareConfig): { color: string; area: number } {
    // ...
    return
}
let mySquare = createSquare({ colooour: "red", width: 100 }); 

如上例所示,createSquare的调用会报错。你可能会认为,参数对象包含wdith属性,类型也正确,没有color属性,并且colooour属性可以视作额外无意义的属性。
尽管如此,TS仍然会认为这样是有BUG的。对象字面量会被特殊对待而且会经过“额外属性检查”,当将它们赋值给变量或作为参数传递的时候。如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
这句话应该很好理解,在上面示例中,调用函数createSquare传入一个通过对象字面量创建的对象时,这时经过额外的属性检查,发现colooour属性不是目标类型包含的属性,报出错误。
解决方式1:使用类型断言

let mySquare = createSquare({ colooour: "red", width: 100 } as SquareConfig);

解决方式2:赋值另外变量

let opt = { colooour: "red", width: 100 };
let mySquare = createSquare(opt);

不同于字面量方式,传入opt对象不会经过额外的属性检查。

以上两种方式都可以绕开检查,更好的实现方式是
为接口SquareConfig添加字符串签名索引:

interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}

这样做的前提是,你要确保这个对象可以具有一些额外的属性,如此一来就允许有任意数量的属性,并且只要不是color和width,那么就无所谓是什么类型。
在处理复杂的对象结构时可以使用上面的技巧绕开检查,但是在简单的代码中尽量不要使用,而是要去修改接口定义来支持额外的属性传入。

函数类型

接口能够描述JavaScript中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。
调用签名好比是一个只有参数列表和返回值类型的函数定义。

interface SearchFunc {
    // 调用签名
    (source: string, subString: string): boolean
}

这个接口就是一个函数类型的接口。
下面来使用这个函数类型的接口。

let mySearch: SearchFunc = function (src: string, sub: string): boolean {
    let result = src.search(sub)
    return result > -1
}

函数的参数名不需要与接口里定义的名字相匹配。但是要求对应位置上的参数类型是兼容的,并且返回值类型要与接口定义的一致。

可索引的类型

与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如a[10]或ageMap["daniel"]。
可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。
索引签名分为两种,一种是数字索引签名,一种是字符串索引签名。
数字索引签名就是在定义一个具有数字索引签名的接口,通过number类型去索引得到一个指定类型的返回值。

interface StringArray {
    // 数字签名
    [index: number]: string
}

let myArray: StringArray = ['bob', 'fred', 'smith'];
let myStr: string = myArray[0];

字符串索引签名与数字索引签名只是索引类型不同,

interface StringArray {
    // 字符串索引签名
    [index: string]: string
}

let myArray: StringArray = { 'a': 'aaa', 'b': 'bbb' };
let myStr22: string = myArray22['a'];
console.log(myStr22) // aaa

这两种就是TS支持的两种索引格式,需要注意的是数字索引返回值类型必须是字符串索引返回值类型的子类型,因为访问数字索引的时候,会将数字转换为字符串。
如:

let myStr: string = myArray[0];
// 相当于
let myStr: string = myArray['0'];

又如:

class Animal {
    name: string
}

class Dog extends Animal {
    breed: string
}

interface NotOkay {
    [x: number]: Dog // 数字签名
    [x: string]: Animal // 字符串签名
    // 数字签名返回的类型要是字符串签名索引返回的子类型
}

let an1 = new Animal()
let do1 = new Dog()
let oo1: NotOkay = { 'bb': an1, 2: do1 }

console.log(oo1[2]) // Dog {} 访问数字签名时,会将数字转换为字符串,
// 也就是为什么数字签名的返回类型要是字符串签名返回类型的子类型
console.log(oo1['bb']) // Animal {}

此外,字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配。
如:

interface NumberDictionary {
  [index: string]: number;
  length: number;    // 可以,length是number类型
  name: string       // 错误,`name`的类型与索引类型返回值的类型不匹配
}

最后你还可以给索引签名设置为只读,防止了给索引赋值:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}

类类型

TS中类也可以去实现接口,让类具有某种约束或者契约。

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

// 类实现接口就要拥有接口中定义的属性和方法
class Clock2 implements ClockInterface {
    currentTime: Date
    constructor(h: number, m: number) {

    }

    setTime(d: Date) {
        this.currentTime = d
    }
}

类是具有两个类型的:静态部分的类型和实例的类型。实现接口的叫实例类型。
类实现接口,接口只描述了类的公共部分(也就是类的实例类型),是不会检查类的私有成员。
实现接口的属性和方法属于类的实例类型。
类的构造器是静态类型。
构造器接口
用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误,因为上面说过,构造器存在于类的静态部分,类只会对实例部分做类型检查。
下面的做法是错误的:

interface ClockConstructor {
   // 构造器签名
   new(hour: number, minute: number)
}

class Clock implements ClockConstructor {
   currentTime: Date;
   constructor(h: number, m: number) { }
}

如何使用构造器接口
我们学过了实例接口,构造器接口。又知道一个类不能直接实现一个构造器接口。那如何使用构造器接口呢?看下面的示例:

// 为实例方法所用
interface ClockInterface {
    tick()
}.
// 为构造函数所用
interface ClockConstructor {
    // 构造函数签名
    new(hour: number, minute: number): ClockInterface
}

// 会检查第一个参数是否符合构造函数签名ClockConstructor
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('tick  Digital');
    }
}

class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log('tick  Analog');
    }
}

let digital = createClock(DigitalClock, 12, 17)
let anglog = createClock(AnalogClock, 7, 32)

因为createClock函数的第一个参数是ClockConstructor类型,在调用createClock时,会检查DigitalClock、AnalogClock是否符合构造函数签名。又因为DigitalClock、AnalogClock类都实现了接口ClockInterface,并且ClockConstructor构造器接口签名返回值类型为ClockInterface,那么DigitalClock、AnalogClock类都是满足条件的。

这样我们就实现了一个工厂方法,用传进的类型创建实例,并且该类型受构造器接口约束。

继承接口

接口也可以相互继承 从一个接口里复制成员到另一个接口,更灵活的将接口分割到一些可重用的模块。并且可以同时继承多个接口。

interface Shape {
    color: string
}

interface PenStroke {
    penWidth: number
}

interface Square extends Shape, PenStroke {
    sideLength: number
}

// 一个接口可以继承多个接口,创建出多个接口的合成接口
let squre = {} as Square
squre.color = 'blue'
squre.sideLength = 10
squre.penWidth = 5.0

混合类型

先前我们提过,接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。

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();
c(10);
c.reset();
c.interval = 5.0;

例子中getCounter返回的对象是Counter类型,该对象即是一个函数类型,同时又是一个对象类型,有reset方法和interval属性。

*接口继承类

接口继承类,使用场景不多。
接口继承类时,会继承类的成员,但不包括实现。好像接口声明了所有类存在的成员,但没有提供具体实现,同时也会也会继承私有和受保护的成。
这也就意味着,一个接口继承了一个拥有私有或者受保护成员的类时,那么这个接口只能够被这个类或其子类实现。
看一个示例:

class Control {
    private state: any
}

interface SelectableControl extends Control {
    select()
}

// 声明Button类并继承Control类,然后实现SelectableControl接口
class Button extends Control implements SelectableControl {
    select() { }
}

// 这个类没有继承Control就实现了接口,是不行的,缺少私有成员state
// class ImageC implements SelectableControl {
//     select() { }
// }

接口SelectableControl继承了Control类,那么就是说接口也继承了类中的成员state。那么只有Control类的子类才能实现这个接口。
上面代码中Button类继承了Control类,那么也就自然的继承了类中的成员state,然后又实现了SelectableControl接口,接口要求实现它的类中有state成员,Button满足,所以这样是正确的。
ImageC类没有继承Control类就去实现SelectableControl接口,接口检查到类中没有成员state,所以报错。

接口相关的内容就是这么多,接口在TS扮演了非常重要的角色,使用广泛。


巴斯光年
274 声望23 粉丝