2 minute read

TypeScript 的核心原则之一是对值所具有的结构进行类型检查。在 TypeScript 里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

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

let myObj = { size: 10, label: "Size 10 Object" }
printLabel(myObj)

类型检查器会查看printLabel的调用。 printLabel有一个参数,并要求这个对象参数有一个名为label类型为string的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。如果需要的参数有过个,而我们传递到少了就会报错

function printLabel(labelledObj: { label: string, size: number }) {
    console.log(labelledObj.label)
}

let myObj = { label: "hello world!" }
printLabel(myObj) // Property 'size' is missing in type '{ label: string; }' but required in type '{ label: string; size: number; }'.

我们在用一下接口的方式来改造上面的函数

interface LabelledValue {
    label: string;
}
function printLabel(labelledObj: LabelledValue) {
    console.log(labelledObj.label)
}
let myObj = { size: 10, label: "hello world!" }
printLabel(myObj)

在这里 LabelledValue 仅仅只是一个名字,与 C#中的接口定义尽管在形式上一样,但是有区别的,C#中使用接口的时候需要实现(implement)这个接口,在 ts 中我们只在意他的形式,用来描述我们需要的值得形式是怎么样的,并且不在意你定义值得顺序,只要存在这个属性并且类型对就行

可选属性

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

就拿上面的那个报错的例子来说吧,假设某些条件下我们是不需要传递size 这个属性的,因此我们就可以如下操作,在 size 后加个?

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

使用可选属性的好处

  • 可以对可能存在的属性进行预定义
  • 可以捕获引用了不存在的属性时的错误

只读属性

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

interface Point {
    readonly x: number;
    readonly y: number;
}
let p: Point = { x: 1, y: 2 }
p.x = 2  // error Cannot assign to 'x' because it is a read-only property.

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

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error! 类型“readonly number[]”中的索引签名仅允许读取
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
// 可以通过断言的方式来让 ro 赋值给别的变量
a = ro as number[]

最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用readonly

额外属性检查

我们看看下面这个例子

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

function createSquare(config: SquareConfig): { color: string, area: number } {
    // ...
}

let mySquare = createSquare({ colour: "red", width: 100 })

这个colour 在这里是毫无意义的,但是 ts 不这么认为的,它会认为这是一个 bug,可能理解为你打错字了,但我们可能的确在这个对象中有这个属性,但是又不想去用它呢。举个实际的例子:我们从接口请求回来的数据中常常会携带一些别的数据,但是我们有时候会直接把这个数据对象作为参数传递给某个方法,如果这个时候报错我们难道要再次对这个数据进行一些处理再去传递吗?显然如果这个数据字段比较多,再次处理很没必要。因此有时候我们会考虑去绕开这个 额外属性检查.

第一种方法,可能细心点的都已经发现了,我们上面也差不多类似的这么写,但是我们把参数先赋给了一个变量,因为这个不会经过额外属性检查,所以编译器不会报错。

let obj = { colour: "red", width: 100 }
let mySquare = createSquare(obj)

第二种使用类型断言

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

第三种添加一个字符串索引签名(最佳

前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。就如我上面说的,如果你传递的参数中除了定义的属性外还有别的,那么建议这么用

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

在像上面一样的简单代码里,你可能不应该去绕开这些检查。 对于包含方法和内部状态的复杂对象字面量来讲,你可能需要使用这些技巧,但是大部额外属性检查错误是真正的 bug。

函数类型

接口能够描述 JavaScript 中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。

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

interface SearchFunc {
    (source: string, subString: string): boolean;
}
let myFunc: SearchFunc
myFunc = function (src: string, subsrc: string): boolean {
    return src.startsWith(subsrc)
}

myFunc("hello world!", "hello") // true

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配,要求对应位置上的参数类型是兼容的

可索引类型(数组型)

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

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

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

let myStr: string = myArray[0]

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

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

类类型

这里的接口实现就和 C#与 Java 中的就基本上一致了,我们需要实现这个接口,在这个接口里可以定义属性和方法,然后在实现该接口的类中实现这些属性和方法。

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

class Clock implements ClockInterface {
    currentTime: 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 = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

接口继承类

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的 private 和 protected 成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

class Control {
    private state: any;
}

interface SelectableControl extends Control {
    select(): void;
}

class Button extends Control implements SelectableControl {
    select() { }
}

class TextBox extends Control {
    select() { }
}

// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl {
    select() { }
}

class Location {

}

Updated: