技术分享

使用TypeScript开发微信小程序(5)——接口(Interface)


TypeScript核心设计原则之一就是类型检查,通过使用接口(Interfaces)可以进行类型检查,满足传统面向对象思想,利于有效开发,有效避免类型转换问题。

在 TypeScript 中,接口的作用就是为类型命名和为代码或第三方代码定义契约。 在编译成 JavaScript 的时候,所有的接口都会被擦除掉,因为 JavaScript 中并没有接口这一概念。

接口

TypeScript 使用接口:

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

以上代码,LabelledValue 接口就好比一个名字,用来描述要求。 它代表了有一个 label 属性且类型为 string 的对象。 并不能像在其它语言里一样,传给 printLabel 的对象实现了 LabelledValue 接口,我只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。

类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。

    interface SquareConfig {
        color?: string;
        width?: number;
    }    function createSquare(config: SquareConfig): { color: string; area: number } {        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" });

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号。

可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。

只读属性

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

    interface Point {
        readonly x: number;
        readonly y: number;
    }    let p1: Point = { x: 10, y: 20 };    // p1.x = 5; // Error

以上代码,通过赋值一个对象字面量来构造一个Point。 赋值后, x和y再也不能被改变了。

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

    let a: number[] = [1, 2, 3, 4];    let ro: ReadonlyArray<number> = a;    // ro[0] = 12; // Error    // ro.push(5); // Error    // ro.length = 100; // Error    // a = ro; // Error

整个ReadonlyArray赋值到一个普通数组也是不可以的需要用类型断言重写:

    a = ro as number[];

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

额外的属性检查

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

绕开这些检查非常简单。 最简便的方法是使用类型断言。然而,最佳的方式是能够添加一个字符串索引签名,前提是能够确定这个对象可能具有某些做为特殊用途使用的额外属性。 如果 SquareConfig 带有 color 和 width 属性,并且还会带有任意数量的其它属性。

    interface SquareConfig2 {
        color?: string;
        width?: number;
        [propName: string]: any;
    }    let squareOptions2 = { colour: "red", width: 100 };    let mySquare2 = createSquare(squareOptions2);

函数类型

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

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

    interface SearchFunc {
        (source: string, subString: string): boolean;
    }

这样定义后,可以像使用其它接口一样使用这个函数类型的接口。

    let mySearch: SearchFunc;
    mySearch = function (src, sub) {        let result = src.search(sub);        if (result == -1) {            return false;
        }        else {            return true;
        }
    }

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果不想指定类型,Typescript 的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc 类型变量。 函数的返回值类型是通过其返回值推断出来的。 如果让这个函数返回数字或字符串,类型检查器会警告函数的返回值类型与 SearchFunc 接口中的定义不匹配。

可索引的类型

与使用接口描述函数类型差不多,也可以描述那些能够“通过索引得到”的类型,可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。

    interface StringArray {
        [index: number]: string;
    }    let myArray: StringArray;
    myArray = ["Bob", "Fred"];    let myStr: string = myArray[0];

以上代码,定义了StringArray接口,它具有索引签名。 这个索引签名表示了当用 number 去索引 StringArray 时会得到 string 类型的返回值。

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

字符串索引签名能够很好的描述 dictionary 模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.property 和 obj[“property”] 两种形式都可以。

可以将索引签名设置为只读,这样就防止了给索引赋值。

    interface NumberDictionary {
        [index: string]: number;
        length: number;        // name: string // Error    }    
    interface ReadonlyStringArray {
        readonly[index: number]: string;
    }

实现接口

与C#或Java里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约。可以在接口中描述一个方法,在类里实现它。

    interface IClock {
        currentTime: Date;
        setTime(d: Date): void;
    }    class Clock implements IClock {
        currentTime: Date;
        setTime(d: Date) {            this.currentTime = d;
        }
        constructor(h: number, m: number) { }
    }

当用构造器签名去定义一个接口并试图定义一个类去实现这个接口时,只对其实例部分进行类型检查。 constructor 存在于类的静态部分,所以不在检查的范围内。 应该直接操作类的静态部分。

    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");
        }
    }    let digital = createClock(DigitalClock, 12, 17);    let analog = createClock(AnalogClock, 7, 32);

以上代码,定义了两个接口, ClockConstructor 为构造函数所用和 ClockInterface 为实例方法所用。 为了方便定义一个构造函数 createClock,它用传入的类型创建实例。因为createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 7, 32)里,会检查AnalogClock是否符合构造函数签名。

扩展接口

和类一样,接口也可以相互扩展。 可以从一个接口里复制成员到另一个接口里,更灵活地将接口分割到可重用的模块里。 一个接口可以继承多个接口,创建出多个接口的合成接口。

    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;

混合类型

接口能够描述JavaScript里丰富的类型, 因为 JavaScript 其动态灵活的特点,一个对象可以同时具有多种类型。 一个对象可以同时做为函数和对象使用,并带有额外的属性。

接口继承类

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

当有一个庞大的继承结构时这很有用,但代码只在子类拥有特定属性时起作用,这个子类除了继承至基类外与基类没有任何关系。

    class Control {
        private state: any;
    }

    interface SelectableControl extends Control {
        select(): void;
    }    class Button extends Control {
        select() { }
    }    class TextBox extends Control {
        select() { }
    }    class Image {
        select() { }
    }    class Location {
        select() { }
    }

以上代码,SelectableControl 包含了 Control 的所有成员,包括私有成员 state。 因为 state 是私有成员,所以只能够是 Control 的子类们才能实现 SelectableControl 接口。 因为只有 Control 的子类才能够拥有一个声明于 Control 的私有成员 state,这对私有成员的兼容性是必需的。

在 Control 类内部,是允许通过 SelectableControl 的实例来访问私有成员 state 的。 实际上, SelectableControl 就像 Control 一样,并拥有一个 select 方法。 Button 和TextBox 类是 SelectableControl 的子类,但 Image 和 Location 类并不是这样的。

把类当做接口使用

类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以能够在允许使用接口的地方使用类。

    class Chart {
        x: number;
        y: number;
    }
    interface LabelChart extends Chart {
        text: string;
    }    let textShape: LabelChart = { x: 1, y: 2, text: 'hello' };

参考资料

  • TypeScript官网
  • TypeScript中文网

其他

  • 完整代码:https://github.com/guyoung/GyWxappCases/tree/master/TypeScript
  • 微信小程序Canvas增强组件WeZRender:https://github.com/guyoung/WeZRender