TypeScript 类型系统

在我们讨论为什么选择 TypeScript的时候就已经谈论过了 TypeScript 类型系统的主要特性。下面是从讨论中提取出来的一些不需要更深层解释的关键点:

  • TypeScript 中的类型系统被设计为可选的,因此你的 JavaScript 就是 TypeScript
  • 当类型错误存在时,TypeScript 不会停止生成 JavaScript,允许你渐进把 JS 更新成 TS

现在让我们开始讲述 TypeScript 类型系统的语法。这次你可以开始马上在代码中使用这些注解并且看到它们的优点。这会为接下来的深入挖掘做准备。

基本注解

就像先前提到的那样,类型会被注解以 :TypeAnnotation 语法。任何在类型声明空间可用的东西都能用作类型注解。

下面的例子展示了类型注解能被用在变量,函数参数和函数返回值上。

var num: number = 123;
function identity(num: number): number {
    return num;
}

原始类型

JavaScript 原始类型很好地体现在 TypeScript 类型系统中。即 stringnumberboolean,如下所示:

var num: number;
var str: string;
var bool: boolean;

num = 123;
num = 123.456;
num = '123'; // Error

str = '123';
str = 123; // Error

bool = true;
bool = false;
bool = 'false'; // Error

数组

TypeScript 为数组提供了专用的类型语法来使你更简单地注解和编档你的代码。语法是后置 [] 于任意有效的类型注解上(例如 :boolean[])。这允许你安全地做任何你通常会做的数组操作,以及从像是赋值一个成员以错误的类型的错误中保护你。如下所示:

var boolArray: boolean[];

boolArray = [true, false];
console.log(boolArray[0]); // true
console.log(boolArray.length); // 2
boolArray[1] = true;
boolArray = [false, false];

boolArray[0] = 'false'; // Error!
boolArray = 'false'; // Error!
boolArray = [true, 'false']; // Error!

接口

接口在 TypeScript 中是核心的方法去组合多个类型注解到一个单独的命名注解上。考虑下面的例子:

interface Name {
    first: string;
    second: string;
}

var name: Name;
name = {
    first: 'John',
    second: 'Doe'
};

name = {           // Error : `second` is missing
    first: 'John'
};
name = {           // Error : `second` is the wrong type
    first: 'John',
    second: 1337
};

这里我们组合了注解 first: string + second: string 到一个新的注解 Name 上,以强制类型检查单独的成员。接口在 TypeScript 有着很多能力,而我们会使用一整章来讲述你可以怎么使用它来获得好处。

行内类型注解

取创建一个新的 interface 而代之,你可以在行内使用 :{ /*Structure*/ } 标注任何东西。之前的例子用行内的方法重写:

var name: {
    first: string;
    second: string;
};
name = {
    first: 'John',
    second: 'Doe'
};

name = {           // Error : `second` is missing
    first: 'John'
};
name = {           // Error : `second` is the wrong type
    first: 'John',
    second: 1337
};

行内的类型用于快速提供一次性类型标注。这可以把你从想一个(可能并不好)的类型名称中解救出来。然而,如果你发现你自己多次放置相同的行内类型标注,最好考虑重构它为一个接口(或者一个 type alias,后面会讲到)。

特殊类型

除了之前提到的原始类型以外,还有一些类型在 TypeScript 里有着特殊意义。它们分别是 anynullundefinedvoid

any

any 类型在 TypeScript 类型系统中保持一个特殊地位。它给了你一个逃离类型系统告诉编译器出现 bug 的方式。any 兼容类型系统中的任意所有类型。这意味着任何东西可以赋值给它它可以赋值给任何东西。例子如下所示:

var power: any;

// 获取任意所有类型
power = '123';
power = 123;

// 兼容所有类型
var num: number;
power = num;
num = power;

如果你正在把 JavaScript 代码移植到 TypeScript,在开始时你会跟 any 有一段友情。然而,别太把这段友情当真因为确保类型安全的决定权在于你。你基本上在告诉编译器别做任何有意义的静态分析

nullundefined

nullundefined JavaScript 关键字会被类型系统简单地看作与类型 any 一样。这些关键字能被赋值到任意其他类型。例子如下所示:

var num: number;
var str: string;

// 这些关键字能被赋值到任何东西
num = null;
str = undefined;

:void

使用 :void 来标识函数不会有任何返回值。

function log(message): void {
    console.log(message);
}

泛型

计算机科学中很多算法和数据结构不依赖于对象的实际类型。然而你仍然想要在不同的变量中制定约束。一个简单的玩具例子是,一个获取一个列表然后返回一个反向列表的函数。这里的约束在于被传入函数的东西和函数返回的东西之间:

function reverse<T>(items: T[]): T[] {
    var toreturn = [];
    for (let i = items.length - 1; i >= 0; i--) {
        toreturn.push(items[i]);
    }
    return toreturn;
}

var sample = [1, 2, 3];
var reversed = reverse(sample);
console.log(reversed); // 3,2,1

// 安全!
reversed[0] = '1';     // Error!
reversed = ['1', '2']; // Error!

reversed[0] = 1;       // Okay
reversed = [1, 2];     // Okay

这里基本上就是说函数 reverse 拿了一个数组(items: T[]),它是某个类型 T 的(注意 reverse<T> 里的类型参数),并且返回了一个类型为 T(注意 : T[])的数组。因为 reverse 函数返回了跟它拿到的同样类型的东西,TypeScript 知道 reversed 变量同样也是类型 number[] 并且会给予它类型安全。同样地如果你传一个 string[] 的数组给 reverse 函数,返回的结果同样也是一个 string[] 的数组,并且你得到了同样的类型安全,如下所示:

var strArr = ['1', '2'];
var reversedStrs = reverse(strArr);

reversedStrs = [1, 2]; // Error!

实际上 JavaScript 数组已经有一个 .reverse 函数,TypeScript 的确使用了泛型来定义它的结构:

interface Array<T> {
 reverse(): T[];
 // ...
}

这意味着你在调用 .reverse 于任何数组时都能得到类型安全,如下所示:

var numArr = [1, 2];
var reversedNums = numArr.reverse();

reversedNums = ['1', '2']; // Error!

我们稍后在 Ambient Declarations 这章展示 lib.d.ts 的时候会讨论更多关于 Array<T> 的话题。

并类型

在 JavaScript 中很普遍的状况是,你想要允许一个属性是多个类型如string 或者 number中的一个。在这里并类型(在类型注解中用 | 标记,例如 string|number)就派上用场了。一个普遍的用例是,能够拿一个单独的对象或者一个对象数组的函数,例如:

function formatCommandline(command: string[]|string) {
    var line = '';
    if (typeof command === 'string') {
        line = command.trim();
    } else {
        line = command.join(' ').trim();
    }

    // 用 line:string 做些事
}

交类型

extend 是一种 JavaScript 中的常见模式,用于你拿到两个对象,然后创建一个新的同时拥有这两个对象特性的对象。交类型允许你以安全的方式去使用这个模式,如下所示:

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U> {};
    for (let id in first) {
        result[id] = first[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            result[id] = second[id];
        }
    }
    return result;
}

var x = extend({ a: "hello" }, { b: 42 });

// x 现在同时有 `a` 和 `b`
var a = x.a;
var b = x.b;

元组类型

JavaScript 没有一等元组支持。人们通常使用数组当作元组。这正是 TypeScript 类型系统所支持的。元组可以使用 :[typeofmember1, typeofmember2] 等来标注。一个元祖可以有任意数量的成员。元组的示例如下:

var nameNumber: [string, number];

// Okay
nameNumber = ['Jenny', 8675309];

// Error!
nameNumber = ['Jenny', '867-5309'];

将这与 TypeScript 中的解构支持合并起来,元组看起来就是一等的了,尽管底层是使用数组实现的。

var nameNumber: [string, number];
nameNumber = ['Jenny', 8675309];

var [name, num] = nameNumber;

类型别名

TypeScript 提供了方便的语法来为你将会不只一次使用的类型注解提供名字。别名是使用 type SomeName = someValidTypeAnnotation 语法创建的。例子如下所示:

type StrOrNum = string|number;

// 使用方法:就像其他类型那样
var sample: StrOrNum;
sample = 123;
sample = '123';

// 检查
sample = true; // Error!

不像 interface,你可以给一个类型以字面意义上的任何类型注解(对于像是并和交类型的东西特别有用)。这里是一些例子使你更熟悉它的语法:

type Text = string | { text: string };
type Coordinates = [number, number];
type Callback = (data: string) => void;

提示:如果你需要有深层的类型注解,使用 interface。为简单的对象结构(例如 Coordinates)使用类型别名只是为了给他们一个语义的名字。

总结

现在你可以开始标注你的大部分 JavaScript 代码,我们可以跳到 TypeScript 类型系统中所有能力的深度细节了。

results matching ""

    No results matching ""