一、描述

接口实际上只是一种类型或者名称约束,并不像其他面向对象的语言那样需要去实现某个定义的接口,而实际上typescript 也只是对其进行检查。

举例,定义一个 interface 如下:

interface LabelValue= {
    label: String
}

function printLabel(val: LabelValue) {
    console.log(val.label)
}

printLabel({label: 'name'})

而如果我将上面的 {label: 'name'} 改成 {labe2l: 'name'} 则会报错如下:

Argument of type '{ labe2l: string; }' is not assignable to parameter of type 'LabelValue'. Object literal may only specify known properties, but 'labe2l' does not exist in type 'LabelValue'. Did you mean to write 'label'?

但是编译完的代码实际上是没有 interface 的相关内容的

function printLabel(val) {
    console.log(val.label);
}
printLabel({ labe2l: 'name' });

二、可选属性

interface 中的属性可以是可选的,也就是说其他业务方在使用的时候,不必要全部传入,这种属性成为可选属性,定义方式如下:

interface DataConfig{
    width?: number,
    height?: number
}

属性名后面加一个问号 ? 表示这个属性是可选的,使用的时候,即使不传入也不会检查错误。

function getConfig(cfg: DataConfig) {
    console.log(cfg);
}

getConfig({})

三、只读属性

只读属性能够要求只有在初始化的时候改变属性的值,其他时间不允许改变。

定义方式是在属性前面加上 readonly

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

如果初始化了一个属性的值,然后再去改变,则会报错:

const p: Point = {x: 1, y: 2};
p.x = 3; // 报错

上面代码会报错 Cannot assign to 'x' because it is a read-only property.

如果需要一个数组创建后不允许变更,甚至内部的属性也不允许变更,typescript 提供了 ReadonlyArray<T> 类型,数组在创建之后不允许任何的变动,即使是子元素也不能变动。

const arr:ReadonlyArray<number> = [1,2,3];

arr[0] = 2; // 报错
ro.push(5); // error!
ro.length = 100; // error!
let b: number[] = arr; // error!

上面检查报错: Index signature in type 'ReadonlyArray<number>' only permits reading. 此时,数组本身也不允许变更,而后面的 push 方法以及长度改变,重新复制等等都是不可以变动的,并且不能将 arr 的引用复制给其他的变量,最后一行代码检查会报错: Type 'ReadonlyArray<number>' is missing the following properties from type 'number[]': pop, push, reverse, shift, and 6 more.

如果要强行实现 let b: number[] = arr 可以借助断言,因为断言是由开发者本身告诉检查器,检查器不会再去检查:

let b: number[] = arr as number[];

如果只是一个变量,想要维持不可变的话,还是使用 const,但是如果是控制属性的不可变性,则可以使用 readonly

三、额外的属性检查

上面的可选属性的例子给了一个报错是 Argument of type '{ labe2l: string; }' is not assignable to parameter of type 'LabelValue'. Object literal may only specify known properties, but 'labe2l' does not exist in type 'LabelValue'. Did you mean to write 'label'?

虽然是配置了可选属性,但是一旦传入了额外的属性 typescript 还是会报错,因为检查到了一个不存在的属性传入。

interface Config {
    width?: number;
    height?: number;
}

const cfg: Config = {width: 100, color: 'red'}; // 报错

要解决这个问题的方式有几种,一种是利用断言:

const cfg: Config = {width: 100, color: 'red'} as Config;

如果不想使用断言,则在定义 interface 的时候,可以增加一个可选的属性配置,而这个属性值是 any 类型:

interface Config {
    width?: number;
    height?: number;
    [propName: string]: any;
}

上面声明了一个 key 是 string 类型,值是 any 类型,声明之后, Config 这个接口可以传入任意数量的属性。

四、函数类型

所谓的函数类型其实约束的是函数的参数及返回值,实际上也是对这个方法的描述:

interface Func {
    (width: number, height: number) : boolean;
}

上面定义的 Func 这个 interface 对函数的描述是拥有 width 和 height 两个参数,类型都是 number,函数的返回值是 boolean。

因此在实例化方法的时候,可以直接通过 Func 来声明函数的 "类型":

const func: Func = function(width: number, height: number) {} // 报错

上面实例化的方法 func 类型检查是会报错 `Type '(width: number, height: number) => void' is not assignable to type 'Func'.
Type 'void' is not assignable to type 'boolean'.` 因为没有指定返回值的类型。

const func: Func = function (width: number, height: number): boolean {
    return false;
}

上面指定了 boolean 的返回值之后,类型检查会通过,不过,此时方法必须返回一个 boolean 类型,因为方法强制了返回 boolean。

函数类型的参数名称并不是强制的,检查的实际上是类型,下面的代码是不会报错误的:

const func: Func = function (w: number, h: number): boolean {
    return false;
}

五、可索引的类型

可索引类型实际上就是用来描述能够“通过索引得到”的类型,比如 a[10] 或者是 ageMap['name']

interface StringArray {
    [index: number]: string
}

上面定义了一个可索引类型的接口,首先名称是 index,而 index 的类型是 number,索引的结果是 string,这里的 string 本身也是对 array 子元素的类型限制。

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

let myStr: string = myArray[0];

定义了接口之后,就能够通过可索引的类型去索引数据,比如通过 myArray[0] 索引就符合规范,0 是 number 并且返回 string,但是下面的代码是汇报错误的:

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

let myStr: string = myArray[2];

TypeScript支持两种索引签名:字符串数字

可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。

也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。

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

class Cat extends Animal {
    name: string;
}

// 错误:使用数值型的字符串索引,有时会得到完全不同的 Animal!
interface NotOkay {
    [x: number]: Animal; // 检查报错: Numeric index type 'Animal' is not assignable to string index type 'Dog'.
    [x: string]: Dog;
}

interface NotOkayTwo {
    [x: number]: Animal;
    [x: string]: Dog;
}

字符串索引签名能够很好的描述 dictionary 模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.propertyobj["property"] 两种形式都可以。 下面的例子里, name 的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:

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

如果为了避免给 interface 的索引赋值,则可以将索引属性设置为 readonly

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