TypeScript: Interface

20th August 2020 at 2:19pm
TypeScript

为什么在 JS 已经有 class 的情况下,TS 还要发明一个 interface 概念呢?原因是对于 JS、Python 这类弱类型语言来讲,它的一大优势是支持 duck typing。而 TS 的 interface 即是用来实现 duck typing 的。

Excess Property Checks

官方的 handbook 提到:

(variable) won't undergo excess property checks

即是说 TS 的属性检查,对 object literal 会比对 variable 严格。不太理解这个设定的原因,于是在 StackOverflow 找到一个 回答

It's by design. In short, Typescript creators made it this way because they know Javascript is a very dynamic language with many such use cases.
Their logic might be like this: if you have a variable, then it may come from some third party and there is not much you can do with it. On the other hand, if you pass object literal, then you are responsible for its correct type.

Indexable Types

Indexable Types 是比较难理解的一环。

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

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

let myStr: string = myArray[0];

上面的代码中定义了一个 StringArray,它可以接受数字作为索引,因此可以是一个数组。同时定义的数组的元素类型为 string。注意 [index: number]: string; 中的 index,可以被任意命名,你叫它 i / x / whatever 都可以。

下面是另外一个例子:

interface StringDict {
    [index: string]: string;
}

let myDict: StringDict = {"a": "hello", "b": "world"};

接口定义中可以揉合 indexable 及非 indexable 的 signature parameter,比如:

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

此时一个 SquareConfig 除了有 colorwidth 两个具名属性外,还可以有任意属性名的其他属性。

注意 index 仅支持两种类型:string 和 number。假如同时存在两种类型的 index 时,number index 属性的类型应该是 string index 属性类型的子类(因为 JS 最终会把 number 转成 string 再做 indexing 的过程)。同时其他属性的类型也应该是 string index 属性类型的子类:

class Animal {
    name: string;
}

class Dog extends Animal {
    breed: string;
}

interface TestingIndexable {
    // Dog 及 Animal 反过来则会报错
    [propName: number]: Dog;
    [propName: string]: Animal;
    
    // 报错,number 不是 Animal 的子类
    p1: number;
    
    // OK
    p2: Dog;
    
    // OK
    p3: Animal;
}

你可以用联合类型来应对一部分场景:

interface NumberDictionary {
    [index: string]: number;
    length: number;    // ok, length is a number
    name: string;      // error, the type of 'name' is not a subtype of the indexer
}

interface NumberOrStringDictionary {
    [index: string]: number | string;
    length: number;    // ok, length is a number
    name: string;      // ok, name is a string
}

你也可以给 index signature 加上只读,可以防止赋值给对象 index:

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

Difference between the static and instance sides of classes

Difference between the static and instance sides of classes 这节讲的东西感觉复杂。

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

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

上面这段代码会报错,因为 Clock 并没有实现一个 new 函数。构造函数(constructor)是 static side of the class,而:

When a class implements an interface, only the instance side of the class is checked.

而当你把类作为一个参数做类型检查时,会检查它的 static side:

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

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

// 这里 DigitalClock / AnalogClock 类,作为参数调用 `createClock`,
// 而 `createClock` 的参数定义它为 `ClockConstrutor` 类,于是 TS 
// 会检查这个类有没有 `ClockConstructor` 中 `new` 所定义的构造函数
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

可以用 class expression 简化它:

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

interface ClockInterface {
    tick();
}

const Clock: ClockConstructor = class Clock implements ClockInterface {
    constructor(h: number, m: number) {}
    tick() {
        console.log("beep beep");
    }
}

Call signature

interface Counter {
    (start: number): string;
}

let counter: Counter = function (start: number) { return `${start}`; }

上面这种不带函数名的 signature((start: number): string)即叫作 call signature。有这个存在时,Counter 的实例必须是 callable 的(即可被当作函数调用的)。interface 中也可以混合 call signature 和其他的 signature,如:

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;