TypeScript——高级类型(1)

交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable同时是 Person 和 Serializable 和 Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员。

我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子:

function extend(first: T, second: U): T & U {

    let result = {};

    for (let id in first) {

        (result)[id] = (first)[id];

    }

    for (let id in second) {

        if (!result.hasOwnProperty(id)) {

            (result)[id] = (second)[id];

        }

    }

    return result;

}

class Person {

    constructor(public name: string) { }

}

interface Loggable {

    log(): void;

}

class ConsoleLogger implements Loggable {

    log() {

        // ...

    }

}

var jim = extend(new Person("Jim"), new ConsoleLogger());

var n = jim.name;

jim.log();

联合类型(Union Types)

联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入 number或 string类型的参数。 例如下面的函数:

/**

* Takes a string and adds "padding" to the left.

* If 'padding' is a string, then 'padding' is appended to the left side.

* If 'padding' is a number, then that number of spaces is added to the left side.

*/

function padLeft(value: string, padding: any) {

    if (typeof padding === "number") {

        return Array(padding + 1).join(" ") + value;

    }

    if (typeof padding === "string") {

        return padding + value;

    }

    throw new Error(`Expected string or number, got '${padding}'.`);

}

padLeft("Hello world", 4); // returns "    Hello world"

padLeft存在一个问题, padding参数的类型指定成了 any。 这就是说我们可以传入一个既不是 number也不是 string类型的参数,但是TypeScript却不报错。

let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错

在传统的面向对象语言里,我们可能会将这两种类型抽象成有层级的类型。 这么做显然是非常清晰的,但同时也存在了过度设计。 padLeft原始版本的好处之一是允许我们传入原始类型。 这样做的话使用起来既简单又方便。 如果我们就是想使用已经存在的函数的话,这种新的方式就不适用了。

代替 any, 我们可以使用 联合类型做为 padding的参数:

/**

* Takes a string and adds "padding" to the left.

* If 'padding' is a string, then 'padding' is appended to the left side.

* If 'padding' is a number, then that number of spaces is added to the left side.

*/

function padLeft(value: string, padding: string | number) {

    // ...

}

let indentedString = padLeft("Hello world", true); // errors during compilation

联合类型表示一个值可以是几种类型之一。 我们用竖线( |)分隔每个类型,所以 number | string | boolean表示一个值可以是 number, string,或 boolean。

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

interface Bird {

    fly();

    layEggs();

}

interface Fish {

    swim();

    layEggs();

}

function getSmallPet(): Fish | Bird {

    // ...

}

let pet = getSmallPet();

pet.layEggs(); // okay

pet.swim();    // errors

这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是 A | B,我们能够 确定的是它包含了 A 和 B中共有的成员。 这个例子里, Bird具有一个 fly成员。 我们不能确定一个 Bird | Fish类型的变量是否有 fly方法。 如果变量在运行时是 Fish类型,那么调用 pet.fly()就出错了。

类型保护与区分类型(Type Guards and Differentiating Types)

联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解是否为 Fish时怎么办? JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如之前提及的,我们只能访问联合类型中共同拥有的成员。

let pet = getSmallPet();

// 每一个成员访问都会报错

if (pet.swim) {

    pet.swim();

}

else if (pet.fly) {

    pet.fly();

}

为了让这段代码工作,我们要使用类型断言:

let pet = getSmallPet();

if ((pet).swim) {

    (pet).swim();

}

else {

    (pet).fly();

}

用户自定义的类型保护

这里可以注意到我们不得不多次使用类型断言。 假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道 pet的类型的话就好了。

TypeScript里的 类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词:

function isFish(pet: Fish | Bird): pet is Fish {

    return (pet).swim !== undefined;

}

在这个例子里, pet is Fish就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。

每当使用一些变量调用 isFish时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。

// 'swim' 和 'fly' 调用都没有问题了

if (isFish(pet)) {

    pet.swim();

}

else {

    pet.fly();

}

注意TypeScript不仅知道在 if分支里 pet是 Fish类型; 它还清楚在 else分支里,一定 不是 Fish类型,一定是 Bird类型。

typeof类型保护

现在我们回过头来看看怎么使用联合类型书写 padLeft代码。 我们可以像下面这样利用类型断言来写:

function isNumber(x: any): x is number {

    return typeof x === "number";

}

function isString(x: any): x is string {

    return typeof x === "string";

}

function padLeft(value: string, padding: string | number) {

    if (isNumber(padding)) {

        return Array(padding + 1).join(" ") + value;

    }

    if (isString(padding)) {

        return padding + value;

    }

    throw new Error(`Expected string or number, got '${padding}'.`);

}

然而,必须要定义一个函数来判断类型是否是原始类型,这太痛苦了。 幸运的是,现在我们不必将 typeof x === "number"抽象成一个函数,因为TypeScript可以将它识别为一个类型保护。 也就是说我们可以直接在代码里检查类型了。

function padLeft(value: string, padding: string | number) {

    if (typeof padding === "number") {

        return Array(padding + 1).join(" ") + value;

    }

    if (typeof padding === "string") {

        return padding + value;

    }

    throw new Error(`Expected string or number, got '${padding}'.`);

}

这些* typeof类型保护*只有两种形式能被识别: typeof v === "typename"和 typeof v !== "typename", "typename"必须是 "number", "string", "boolean"或 "symbol"。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

instanceof类型保护

如果你已经阅读了 typeof类型保护并且对JavaScript里的 instanceof操作符熟悉的话,你可能已经猜到了这节要讲的内容。

instanceof类型保护是通过构造函数来细化类型的一种方式。 比如,我们借鉴一下之前字符串填充的例子:

interface Padder {

    getPaddingString(): string

}

class SpaceRepeatingPadder implements Padder {

    constructor(private numSpaces: number) { }

    getPaddingString() {

        return Array(this.numSpaces + 1).join(" ");

    }

}

class StringPadder implements Padder {

    constructor(private value: string) { }

    getPaddingString() {

        return this.value;

    }

}

function getRandomPadder() {

    return Math.random() < 0.5 ?

        new SpaceRepeatingPadder(4) :

        new StringPadder("  ");

}

// 类型为SpaceRepeatingPadder | StringPadder

let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {

    padder; // 类型细化为'SpaceRepeatingPadder'

}

if (padder instanceof StringPadder) {

    padder; // 类型细化为'StringPadder'

}

instanceof的右侧要求是一个构造函数,TypeScript将细化为:

此构造函数的 prototype属性的类型,如果它的类型不为 any的话

构造签名所返回的类型的联合

以此顺序。

你可能感兴趣的:(TypeScript——高级类型(1))