TypeScript类型

515 阅读19分钟

基础

TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。如下面的例子

let x = 3;

变量x的类型被推断为数字。 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。

大多数情况下,类型推论是直截了当地。 后面的小节,我们会浏览类型推论时的细微差别。

最佳通用类型

当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。例如,

let x = [0, 1, null];

为了推断x的类型,我们必须考虑所有元素的类型。 这里有两种选择: number和null。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。

由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。例如:

let zoo = [new Rhino(), new Elephant(), new Snake()];

这里,我们想让zoo被推断为Animal[]类型,但是这个数组里没有对象是Animal类型的,因此不能推断出这个结果。 为了更正,当候选类型不能使用的时候我们需要明确的指出类型:

let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];

如果没有找到最佳通用类型的话,类型推断的结果为联合数组类型,(Rhino | Elephant | Snake)[]。

上下文类型

TypeScript类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。比如:

window.onmousedown = function(mouseEvent) {
    console.log(mouseEvent.button);  //<- Error
};

这个例子会得到一个类型错误,TypeScript类型检查器使用Window.onmousedown函数的类型来推断右边函数表达式的类型。 因此,就能推断出 mouseEvent参数的类型了。 如果函数表达式不是在上下文类型的位置, mouseEvent参数的类型需要指定为any,这样也不会报错了。

如果上下文类型表达式包含了明确的类型信息,上下文的类型被忽略。 重写上面的例子:

window.onmousedown = function(mouseEvent: any) {
    console.log(mouseEvent.button);  //<- Now, no error is given
};

这个函数表达式有明确的参数类型注解,上下文类型被忽略。 这样的话就不报错了,因为这里不会使用到上下文类型。

上下文归类会在很多情况下使用到。 通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。比如:

function createZoo(): Animal[] {
    return [new Rhino(), new Elephant(), new Snake()];
}


这个例子里,最佳通用类型有4个候选者:Animal,Rhino,Elephant和Snake。 当然, Animal会被做为最佳通用类型。

高级类型

交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable同时是 Person 和 Serializable 和 Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员。您将主要看到用于mixins的交集类型和其他不适合经典面向对象模具的概念。(在JavaScript中有很多这些!)这是一个简单的例子,展示了如何创建mixin:

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{}

    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    
    return result
} 

class AdvancedTypesClass {
    constructor(public name: string){}
}

interface LoggerInterface {
    log(): void;
}

class AdvancedTypesLoggerClass implements LoggerInterface {
    log(): void {
        console.log('console logging');
    }
}

var logger = new AdvancedTypesLoggerClass();

var extend1 = extend(new AdvancedTypesClass("string"), new AdvancedTypesLoggerClass());
var e = extend1.name;
console.log(e);
extend1.log();


编译运行,注意这里要编译运行,我使用ts-node已经不能运行成功了。可能是哪里配置的有问题,具体步骤如下。

tsc ./src/advanced_types_1.ts
$ node ./src/advanced_types_1.js
string
console logging

联合类型(Union Types)

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

/**
 * 为给定的字符串左侧添加"padding"
 * 如果"padding"是一个字符串,则添加将字符串添加到给定字符串的左侧
 * 如果"padding"是一个数字,则添加padding个数量的空格到给定字符串的左侧
 */
function padLeft(value: string, padding: any) {
    if (typeof padding === 'string') {
        return padding + value;
    }

    if (typeof padding === 'number') {
        return Array(padding + 1).join(' ') + value;
    }

    throw new Error(`Excepted string or number, got ${padding}`);
}
console.log("|" + padLeft("string", 4) + "|");
console.log("|" + padLeft("string", "a") + "|");


编译并运行后得到如下结果

$ tsc ./src/advanced_types_1.ts && node ./src/advanced_types_1.js
|    string|
|astring|

padLeft有一个问题,就是padding这个参数是一个any类型,那就意味着我们可以在传递参数的时候,参数的类型可以是number或者是string,而TypeScript将会正常解析,
如果如下的方式调用,编译的时候是可以正常解析的,但是运行的时候回报错

padLeft("Hello world", true);

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

/**
 * 为给定的字符串左侧添加"padding"
 * 如果"padding"是一个字符串,则添加将字符串添加到给定字符串的左侧
 * 如果"padding"是一个数字,则添加padding个数量的空格到给定字符串的左侧
 */
function padLeft(value: string, padding: string | number) {
    if (typeof padding === 'string') {
        return padding + value;
    }

    if (typeof padding === 'number') {
        return Array(padding + 1).join(' ') + value;
    }

    throw new Error(`Excepted string or number, got ${padding}`);
}
console.log("|" + padLeft("string", 4) + "|");
console.log("|" + padLeft("string", "a") + "|");
console.log("|" + padLeft("string", true) + "|");


编译并运行后得到如下结果

$ tsc ./src/advanced_types_1.ts && node ./src/advanced_types_1.js
src/advanced_types_1.ts:65:37 - error TS2345: Argument of type 'true' is not assignable to parameter of type 'string | number'.

65 console.log("|" + padLeft("string", true) + "|");

从实例演示可以看出,当传入一个boolean类型值的时候,在编辑的时候TypeScript就做出了判断,表示boolean类型的参数不被支持

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

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

interface Type1 {
    func1(): void;
    func2(): void;
}

interface Type2 {
    func3(): void;
    func2(): void;
}

class Type1Class implements Type1 {
    func1(): void {
        console.log('func1 run');
    }

    func2(): void {
        console.log('func2 run');
    }
}

class Type2Class implements Type2 {
    func3(): void {
        console.log('func1 run');
    }

    func2(): void {
        console.log('func2 run');
    }
}

function getSomeType(type: string): Type1Class | Type2Class {
    if (type === '1') {
        return new Type1Class();
    }

    if (type === '2') {
        return new Type2Class();
    }

    throw new Error(`Excepted Type1Class or Type2Class, got ${type}`);
}

let type = getSomeType('1');
type.func2();
type.func1(); // 报错


编译并运行后得到如下结果

$ tsc ./src/advanced_types_1.ts
src/advanced_types_1.ts:111:6 - error TS2551: Property 'func1' does not exist on type 'Type1Class | Type2Class'. Did you mean 'func2'?
  Property 'func1' does not exist on type 'Type2Class'.

111 type.func1();


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

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

从上一篇文章【TypeScript基础入门之高级类型的交叉类型和联合类型】的分享中我们可以了解到,联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解是否为某个类型时怎么办? JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如上一篇文章文章【TypeScript基础入门之高级类型的交叉类型和联合类型】中之前提及的,我们只能访问联合类型中共同拥有的成员。如下实例,访问任何一个非公有成员,程序编译的时候都会报错

interface Type1 {
    func1(): void;
    func2(): void;
}

interface Type2 {
    func3(): void;
    func2(): void;
}

class Type1Class implements Type1 {
    func1(): void {
        console.log('func1 run');
    }

    func2(): void {
        console.log('func2 run');
    }
}

class Type2Class implements Type2 {
    func3(): void {
        console.log('func1 run');
    }

    func2(): void {
        console.log('func2 run');
    }
}

function getSomeType(type: string): Type1Class | Type2Class {
    if (type === '1') {
        return new Type1Class();
    }

    if (type === '2') {
        return new Type2Class();
    }

    throw new Error(`Excepted Type1Class or Type2Class, got ${type}`);
}

let type = getSomeType('1');
type.func2();
if (type.func1) {
    type.func1(); // 报错
} else if (type.func3) {
    type.func3(); // 报错
}


编译并运行后得到如下结果

$ tsc ./src/advanced_types_2.ts
src/advanced_types_2.ts:45:10 - error TS2551: Property 'func1' does not exist on type 'Type1Class | Type2Class'. Did you mean 'func2'?
  Property 'func1' does not exist on type 'Type2Class'.

45 if (type.func1) {
            ~~~~~

src/advanced_types_2.ts:46:10 - error TS2551: Property 'func1' does not exist on type 'Type1Class | Type2Class'. Did you mean 'func2'?
  Property 'func1' does not exist on type 'Type2Class'.

46     type.func1(); // 报错
            ~~~~~

src/advanced_types_2.ts:47:17 - error TS2551: Property 'func3' does not exist on type 'Type1Class | Type2Class'. Did you mean 'func2'?
  Property 'func3' does not exist on type 'Type1Class'.

47 } else if (type.func3) {
                   ~~~~~

src/advanced_types_2.ts:48:10 - error TS2551: Property 'func3' does not exist on type 'Type1Class | Type2Class'. Did you mean 'func2'?
  Property 'func3' does not exist on type 'Type1Class'.

48     type.func3(); // 报错

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

interface Type1 {
    func1(): void;
    func2(): void;
}

interface Type2 {
    func3(): void;
    func2(): void;
}

class Type1Class implements Type1 {
    func1(): void {
        console.log('func1 run');
    }

    func2(): void {
        console.log('func2 run');
    }
}

class Type2Class implements Type2 {
    func3(): void {
        console.log('func1 run');
    }

    func2(): void {
        console.log('func2 run');
    }
}

function getSomeType(type: string): Type1Class | Type2Class {
    if (type === '1') {
        return new Type1Class();
    }

    if (type === '2') {
        return new Type2Class();
    }

    throw new Error(`Excepted Type1Class or Type2Class, got ${type}`);
}

let type = getSomeType('1');
type.func2();
if ((<Type1Class>type).func1) {
    (<Type1Class>type).func1();
} else if ((<Type2Class>type).func3) {
    (<Type2Class>type).func3();
}

编译并运行后得到如下结果

$ tsc ./src/advanced_types_2.ts && node ./src/advanced_types_2.js
func2 run
func1 run  

用户自定义的类型保护

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

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

function isType1(type: Type1Class | Type2Class): type is Type1Class {
    return (<Type1Class>type).func1 !== undefined;
}

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

每当使用一些变量调用isType1时,如果原始类型兼容,TypeScript会将该变量缩小到该特定类型。如下

if(isType1(type)) {
    type.func1()
} else {
    type.func3();
}

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

typeof类型保护

我以上篇文章[TypeScript基础入门之高级类型的交叉类型和联合类型]的padLeft代码的代码为例,看看如何使用联合类型来改写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 (isString(padding)) {
        return padding + value;
    }

    if (isNumber(padding)) {
        return Array(padding + 1).join(' ') + value;
    }

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

console.log("|" + padLeft("string", 4) + "|");
console.log("|" + padLeft("string", "a") + "|");

编译并运行后得到如下结果

$ tsc ./src/advanced_types_2.ts && node ./src/advanced_types_2.js
|    string|
|astring|    

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

function padLeft(value: string, padding: string | number) {
    if (typeof padding === 'string') {
        return padding + value;
    }

    if (typeof padding === 'number') {
        return Array(padding + 1).join(' ') + value;
    }

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

这些**typeof类型保护**只有两种形式能被识别:

typeof v === "typename"

typeof v !== "typename"

, "typename"必须是"number","string","boolean"或"symbol"。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

instanceof类型保护

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

interface PadInterface {
    getPadString(): string;
}

class SpacePad implements PadInterface {
    constructor(private num: number){}
    getPadString(): string {
        return Array(this.num + 1).join(' ');
    }
}

class StringPad implements PadInterface {
    constructor(private string: string) { }
    getPadString(): string {
        return this.string;
    }
}

function getRandomPad() {
    return Math.random() < 0.5 ? 
    new SpacePad(5) :
    new StringPad(" ");
}

let pad: PadInterface = getRandomPad();
if (pad instanceof SpacePad) {
    console.log("|" + pad.getPadString() + "string|")
}

if (pad instanceof StringPad) {
    console.log("|" + pad + "string|")
}


第一次编译并运行后得到如下结果

$ tsc ./src/advanced_types_2.ts && node ./src/advanced_types_2.js
|     string|

第二次编译并运行后得到如下结果

$ tsc ./src/advanced_types_2.ts && node ./src/advanced_types_2.js
| string|

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

  • 此构造函数的prototype属性的类型,如果它的类型不为any的话
  • 构造签名所返回的类型的联合

以此顺序。

高级类型

可null类型(Nullable Types)

TypeScript具有两种特殊的类型,null和undefined,它们分别具有值null和undefined。 默认情况下,类型检查器认为null与undefined可以赋值给任何类型。
null与undefined是所有其它类型的一个有效值。 这也意味着,你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。
null的发明者,Tony Hoare,称它为价值亿万美金的错误。

--strictNullChecks

标记可以解决此错误:当你声明一个变量时,它不会自动地包含null或undefined。
你可以使用联合类型明确的包含它们,如下

let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以

sn = undefined; // error, 'undefined'不能赋值给'string | null'

注意,按照JavaScript的语义,TypeScript会把null和undefined区别对待。

string | null

string | undefined

string | undefined | null

是不同的类型。

可选参数和可选属性

使用了

--strictNullChecks

,可选参数会被自动地加上

| undefined

:

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'

可选属性也会有同样的处理:

class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

类型保护和类型断言

由于可以为null的类型是通过联合类型实现,那么你需要使用类型保护来去除 null。 幸运地是这与在JavaScript里写的代码一致:

function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    } else {
        return sn;
    }
}

这里很明显地去除了null,你也可以使用||运算符:

function f(sn: string | null): string {
    return sn || "default";
}

如果编译器不能够去除null或undefined,你可以使用类型断言手动去除。 语法是添加!后缀:

identifier!

从identifier的类型里去除了null和undefined:

先看第一个失败的例子

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet; // error, 'name' is possibly null
  }
  name = name || "Bob";
  return postfix("great");
}


在看下第二个成功的例子

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

上面的例子使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时name的类型。

类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

type Name = string;
type NameFunc = () => string;
type NameOrFunc = Name | NameFunc;
function getName(n: NameOrFunc): Name {
    if (typeof n === 'string') {
        return n;
    }

    return n();
}

起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。

同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:

type Container<T> = { value: T };

我们也可以使用类型别名来在属性里引用自己:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

然而,类型别名不能出现在声明右侧的任何地方。

type Yikes = Array<Yikes>; // error

接口 vs. 类型别名

像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。

其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced上,显示它返回的是 Interface,但悬停在 aliased上时,显示的却是对象字面量类型。

type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

另一个重要区别是类型别名不能被 extends和 implements(自己也不能 extends和 implements其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。

另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! should not pass null or undefined.
        }
    }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here

你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

字符串字面量类型还可以用于区分函数重载:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}

数字字面量类型

TypeScript还具有数字字面量类型。

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
    // ...
}

我们很少直接这样使用,但它们可以用在缩小范围调试bug的时候:

function foo(x: number) {
    if (x !== 1 || x !== 2) {
        //         ~~~~~~~
        // Operator '!==' cannot be applied to types '1' and '2'.
    }
}

换句话说,当x与2进行比较的时候,它的值必须为1,这就意味着上面的比较检查是非法的。

枚举成员类型

如我们在 枚举一节里提到的,当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。

在我们谈及“单例类型”的时候,多数是指枚举成员类型和数字/字符串字面量类型,尽管大多数用户会互换使用“单例类型”和“字面量类型”。

可辨识联合(Discriminated Unions)

你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:

1. 具有普通的单例类型属性— 可辨识的特征。
2. 一个类型别名包含了那些类型的联合— 联合。
3. 此属性上的类型保护。

interface Interface1 {
  kind: 'interface1',
  property1: number,
}

interface Interface2 {
  kind: 'interface2',
  property2: number,
  property3: number,
}

interface Interface3 {
  kind: 'interface3',
  property4: number,
  property5: number,
}

首先我们声明了将要联合的接口。 每个接口都有 kind属性但有不同的字符串字面量类型。 kind属性称做 可辨识的特征或 标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:

type Type = Interface1 | Interface2 | Interface3;

现在我们使用可辨识联合:

function getType(i: Type) {
  switch (i.kind) {
    case "interface1":
      return i.property1 * i .property1;
    case "interface2":
      return i.property2 * i.property3;
    case "interface3":
      return i.property4 * i.property5;
  }
}

完整性检查

当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了Interface4到Type,我们同时还需要更新getType:

interface Interface4 {
  kind: 'interface4',
  property6: number,
}


type Type = Interface1 | Interface2 | Interface3 | Interface4;

function getType(i: Type) {
  switch (i.kind) {
    case "interface1":
      return i.property1 * i .property1;
    case "interface2":
      return i.property2 * i.property3;
    case "interface3":
      return i.property4 * i.property5;
  }
}


有两种方式可以实现。 首先是启用 --strictNullChecks并且指定一个返回值类型:

function getType(i: Type): number { // error: returns number | undefined
  switch (i.kind) {
    case "interface1":
      return i.property1 * i .property1;
    case "interface2":
      return i.property2 * i.property3;
    case "interface3":
      return i.property4 * i.property5;
  }
}

因为 switch没有包涵所有情况,所以TypeScript认为这个函数有时候会返回 undefined。 如果你明确地指定了返回值类型为 number,那么你会看到一个错误,因为实际上返回值的类型为 number | undefined。 然而,这种方法存在些微妙之处且 --strictNullChecks对旧代码支持不好。第二种方法使用 never类型,编译器用它来进行完整性检查:

function assertNever(x: never): never {
  throw new Error("Unexpected object: " + x);
}

function getType(i: Type): number { // error: returns number | undefined
  switch (i.kind) {
    case "interface1":
      return i.property1 * i .property1;
    case "interface2":
      return i.property2 * i.property3;
    case "interface3":
      return i.property4 * i.property5;
    default:
      return assertNever(i); // error here if there are missing cases
  }
}

这里, assertNever检查 s是否为 never类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个case,那么 s将具有一个真实的类型并且你会得到一个错误。 这种方式需要你定义一个额外的函数,但是在你忘记某个case的时候也更加明显。**Tips**
上面的代码是根据官网的逻辑写的,奇怪的是最后一步居然在编译的时候报错了。报错信息如下

$ tsc src/advanced_types_4.ts
src/advanced_types_4.ts:38:26 - error TS2345: Argument of type 'Interface4' is not assignable to parameter of type 'never'.

38       return assertNever(i); // error here if there are missing cases
                            ~

多态的this类型

多态的this类型表示的是某个包含类或接口的子类型。 这被称做F-bounded多态性。 它能很容易的表现连贯接口间的继承,比如。 在下面的例子里,在每个操作之后都返回this类型:

class Query {
  public whereCon: Array<string> = [];

  public constructor(protected tableName: string = '') { }

  public andWhere(key: string, value: string) {
    this.whereCon.push(`${key}=${value}`);
    return this;
  }

  public orWhere(key: string, value: string) {
    this.whereCon.push(`OR ${key}=${value}`);
    return this;
  }
  
  public inWhere(key: string, value: string) {
    this.whereCon.push(`AND ${key} IN (${value})`);
    return this;
  }

  public getSQL(): string {
    return `SELECT * FROM ${this.tableName} WHERE ${this.whereCon.join(' ')}`;
  }

  // ... 其他的操作
}

let generateSQL = new Query('table_name')
  .andWhere('key1', 'value1')
  .orWhere('key2','value2')
  .inWhere('key3','value3')
  .getSQL();

console.log(generateSQL);


运行后输入结果如下

$ npx ts-node ./src/advanced_types_5.ts
SELECT * FROM table_name WHERE key1=value1 OR key2=value2 AND key3 IN (value3)

这个类当然还是有点缺陷的,但是我们可以看出这个特性的使用方式由于这个类使用了this类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。

class TQuery extends Query {
  public constructor(tableName: string = '') {
    super(tableName);
  }

  public getUpdateSql(key: string, value: string) {
    return `UPDATE ${this.tableName} SET ${key}=${value} WHERE ${this.whereCon.join(' ')}`;
  }

  // ... 其他的操作
}

let generateSQL = new TQuery('table_name')
  .andWhere('key1', 'value1')
  .orWhere('key2', 'value2')
  .inWhere('key3', 'value3')
  .getUpdateSql('key4', 'value4');
console.log(generateSQL);

运行后输入结果如下

$ npx ts-node ./src/advanced_types_5.ts
UPDATE table_name SET key4=value4 WHERE key1=value1 OR key2=value2 AND key3 IN (value3)

如果没有this类型,TQuery就不能够在继承Query的同时还保持接口的连贯性。 inWhere将会返回Query,它并没有getUpdateSql方法。 然而,使用this类型,inWhere会返回 this,在这里就是 TQuery。

索引类型(Index types)

使用索引类型,编译器就能够检查使用了动态属性名的代码。 例如,一个常见的JavaScript模式是从对象中选取属性的子集。

function pluck(o, names) {
    return names.map(n => o[n]);
}

下面是如何在TypeScript里使用此函数,通过 索引类型查询和 索引访问操作符:

function pluck<T, K extends keyof T>(o:T, names: K[]): T[K][] {
  return names.map(n => o[n])
}

interface Interface1 {
  name: string;
  age: number;
}

let i: Interface1 = {
  name: "A",
  age: 1,
}

let pluckStr: string[] = pluck(i, ['name'])
console.log(pluckStr)

运行后输出如下

[ 'A' ]

编译器会检查 name是否真的是Interface1的一个属性。 本例还引入了几个新的类型操作符。 首先是 keyof T, 索引类型查询操作符。 对于任何类型 T, keyof T的结果为 T上已知的公共属性名的联合。 例如:

let someProps: keyof Interface1; // 'name' | 'age'

keyof Interface1是完全可以与'name'|'age'互相替换的。 不同的是如果你添加了其它的属性到Interface1,例如address: string,那么 keyof Interface1会自动变为'name'|'age'|'address'。 你可以在像pluck函数这类上下文里使用 keyof,因为在使用之前你并不清楚可能出现的属性名。 但编译器会检查你是否传入了正确的属性名给 pluck:

pluck(i, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'

第二个操作符是T[K],索引访问操作符。 在这里,类型语法反映了表达式语法。 这意味着 i['name']具有类型Interface1['name'] — 在我们的例子里则为string类型。 然而,就像索引类型查询一样,你可以在普通的上下文里使用 T[K],这正是它的强大所在。 你只要确保类型变量 K extends keyof T就可以了。 例如下面 getProperty函数的例子:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name]; // o[name] is of type T[K]
}

getProperty里的 o: T和 name: K,意味着 o[name]: T[K]。 当你返回 T[K]的结果,编译器会实例化键的真实类型,因此 getProperty的返回值类型会随着你需要的属性改变。

let name: string = getProperty(i, 'name');
let age: number = getProperty(i, 'age');
let unknown = getProperty(i, 'unknown'); // error, 'unknown' is not in 'name' | 'age'

索引类型和字符串索引签名

keyof和T[K]与字符串索引签名进行交互。如果你有一个带有字符串索引签名的类型,那么 keyof T会是 string。并且T[string]为索引签名的类型:

interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number

映射类型

一个常见的任务是将一个已知的类型每个属性都变为可选的:

interface PersonPartial {
    name?: string;
    age?: number;
}

或者我们想要一个只读版本:

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}

这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为 readonly类型或可选的。 下面是一些例子:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}

像下面这样使用:

type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

下面来看看最简单的映射类型和它的组成部分:

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

它的语法与索引签名的语法类型,内部使用了 for .. in。 具有三个部分:

1. 类型变量 K,它会依次绑定到每个属性。
2. 字符串字面量联合的 Keys,它包含了要迭代的属性名的集合。
3. 属性的结果类型。
在这个简单的例子里, Keys是硬编码的的属性名列表并且属性类型永远是boolean,因此这个映射类型等同于:

type Flags = {
    option1: boolean;
    option2: boolean;
}

在真正的应用里,可能不同于上面的 Readonly或 Partial。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是 keyof和索引访问类型要做的事情:

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }

但它更有用的地方是可以有一些通用版本。

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

在这些例子里,属性列表是 keyof T且结果类型是 T[P]的变体。 这是使用通用映射类型的一个好模版。 因为这类转换是 同态的,映射只作用于 T的属性而没有其它的。 编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。 例如,假设 Person.name是只读的,那么 Partial<Person>.name也将是只读的且为可选的。

下面是另一个例子, T[P]被包装在 Proxy<T>类里:

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);

注意 Readonly<T>和 Partial<T>用处不小,因此它们与 Pick和 Record一同被包含进了TypeScript的标准库里:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
type Record<K extends string, T> = {
    [P in K]: T;
}

Readonly, Partial和 Pick是同态的,但 Record不是。 因为 Record并不需要输入类型来拷贝属性,所以它不属于同态:

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。

由映射类型进行推断

现在你了解了如何包装一个类型的属性,那么接下来就是如何拆包。 其实这也非常容易:

function unproxify<T>(t: Proxify<T>): T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}

let originalProps = unproxify(proxyProps);

注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。