Jade Dungeon

Typescript基础

基础类型

布尔值

最基本的数据boolean类型就是简单的true/false值。

let isDone: boolean = false;

数字

TypeScript和JavaScript一样里的所有数字都是浮点数number。 除了支持:

  • 十进制和十六进制字面量
  • 二进制和八进制字面量(ECMAScript 2015引入)。
let decLiteral:    number = 6;
let hexLiteral:    number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral:  number = 0o744;

字符串

使用双引号(")或单引号(')表示字符串string

let name: string = "bob";
name = "smith";

还可以用「反引号」包围的模版字符串, 它可以定义多行文本和${ expr }这种形式嵌入表达式。

let name    : string = `Gene`;
let age     : number = 37;

多行文本:

let sentence: string = `Hello, my name is ${ name }.

I'll be ${ age + 1 } years old next month.`;

等同于:

let sentence: string = "Hello, my name is " + name + ".\n\n" +
    "I'll be " + (age + 1) + " years old next month.";

数组

有两种方式可以定义数组。

第一种,类型[]

let list: number[] = [1, 2, 3];

第二种方式是使用数组泛型,Array<元素类型>

let list: Array<number> = [1, 2, 3];

元组 Tuple

各个成员类型不同的数据结构,如stringnumber类型的元组。

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

当访问一个已知索引的元素,会得到正确的类型:

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

当访问一个越界的元素,会使用联合类型替代:

x[3] = 'world';       // OK, 字符串可以赋值给(string | number)类型

console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString

x[6] = true;          // Error, 布尔不是(string | number)类型

联合类型是高级主题,以后再详细说。

枚举

enum类型默认情况下,从0开始为元素编号。

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

手动的指定成员的数值。 例如,从1开始编号:

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;

或者,全部都采用手动赋值:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

根据数值为2查找相应的名字:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];

alert(colorName);  // 显示'Green'因为上面代码里它的值是2

任意值any

any类型跳过类型检查的变量:

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
  • any类型与Object类型都可以被赋任意值。
  • Object类型上面不能调用任意的方法,即便它真的有这些方法
let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.

当只知道一部分数据的类型时也可以用any类型。 比如,一个数组包含了不同的类型的数据:

let list: any[] = [1, true, "free"];

list[1] = 100;

空值void

voidany相反,表示没有任何类型。

函数没有返回值时通常返回值类型是void

function warnUser(): void {
    alert("This is my warning message");
}

void类型的变量只能被赋予undefinednull

let unusable: void = undefined;

NullUndefined

TypeScript里, undefinednull两者各自有自己的类型分别叫做undefinednull

void相似,它们的本身的类型用处不是很大:

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

默认情况下nullundefined是所有类型的子类型。 可以把nullundefined赋值给number类型的变量。

编译选项--strictNullChecks限制nullundefined 只能赋值给void和它们自己。 这能避免很多常见的错误。

更安全的做法是在可能多种类型的情况下, 比如值可能是stringnullundefined时, 使用联合类型string | null | undefined。 再次说明,稍后再介绍联合类型。

注意:尽可能地使用--strictNullChecks

Never

never类型表示的是那些永不存在的值的类型。 例如:

  • 总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;
  • 当变量被永不为真的类型保护所约束时也可能是never类型。

注意:

  • never类型是任何类型的子类型,也可以赋值给任何类型;
  • 除了never本身之外没有类型是never的子类型或可以赋值给never类型。
  • 即使any也不可以赋值给never

下面是一些返回never类型的函数:

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
    return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
    }
}

类型断言

类型断言好比其它语言里的类型转换。 没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设程序员已经进行了必须的检查。

类型断言有两种形式。 其一是「尖括号」语法:

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

另一个为as语法:

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;
  • 两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;
  • 然而,当你在TypeScript里使用JSX时,只有as语法断言是被允许的。

关于let

使用let关键字来代替JavaScript关键字var,很多常见的问题都可以解决。

变量声明

var 声明

一直以来我们都是通过var关键字定义JavaScript变量。

var a = 10;

也可以在函数内部定义变量:

function f() {
    var message = "Hello, world!";

    return message;
}

也可以在其它函数内部访问相同的变量。

function f() {
    var a = 10;
    return function g() {
        var b = a + 1;
        return b;
    }
}

var g = f();
g(); // returns 11;

上面的例子里,g可以获取到f()函数里定义的a变量。 每当g()被调用时, 它都可以访问到f里的a变量。 即使当gf已经执行完后才被调用, 它仍然可以访问及修改a

function f() {
    var a = 1;

    a = 2;
    var b = g();
    a = 3;

    return b;

    function g() {
        return a;
    }
}

f(); // returns 2

作用域规则

对于熟悉其它语言的人来说,var声明有些奇怪的作用域规则。 看下面的例子:

function f(shouldInitialize: boolean) {
    if (shouldInitialize) {
        var x = 10;
    }

    return x;
}

f(true );    // returns '10'
f(false);    // returns 'undefined'

有些读者可能要多看几遍这个例子。 变量x是定义在if语句里面, 但是我们却可以在语句的外面访问它。

这是因为var声明可以在包含它的函数,模块, 命名空间或全局作用域内部任何位置被访问(我们后面会详细介绍), 包含它的代码块对此没有什么影响。 有些人称此为var作用域或函数作用域。 函数参数也使用函数作用域。

这些作用域规则可能会引发一些错误。 其中之一就是,多次声明同一个变量并不会报错:

function sumMatrix(matrix: number[][]) {
    var sum = 0;
    for (var i = 0; i < matrix.length; i++) {
        var currentRow = matrix[i];
        for (var i = 0; i < currentRow.length; i++) {
            sum += currentRow[i];
        }
    }

    return sum;
}

这里很容易看出一些问题,里层的for循环会覆盖变量i, 因为所有i都引用相同的函数作用域内的变量。 有经验的开发者们很清楚, 这些问题可能在代码审查时漏掉,引发无穷的麻烦。

变量获取怪异之处

快速的猜一下下面的代码会返回什么:

for (var i = 0; i < 10; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}

介绍一下,setTimeout会在若干毫秒的延时后执行一个函数(等待其它代码执行完毕)。

好吧,看一下结果:

10
10
10
10
10
10
10
10
10
10

很多JavaScript程序员对这种行为已经很熟悉了,但如果你很不解,你并不是一个人。 大多数人期望输出结果是这样:

0
1
2
3
4
5
6
7
8
9

还记得我们上面讲的变量获取吗?

每当g被调用时,它都可以访问到f里的a变量。

让我们花点时间考虑在这个上下文里的情况:

setTimeout在若干毫秒后执行一个函数, 问题是当第一次调用setTimeout等待的干毫秒后for循环已经结束了。i的值为10。 所以当函数被调用的时候,它会打印出10

一个通常的解决方法是使用立即执行的函数表达式(IIFE)来捕获每次迭代时i的值:

for (var i = 0; i < 10; i++) {
    // capture the current state of 'i'
    // by invoking a function with its current value
    (function(i) {
        setTimeout(function() { console.log(i); }, 100 * i);
    })(i);
}

这种奇怪的形式我们已经司空见惯了。 参数i会覆盖for循环里的i, 但是因为我们起了同样的名字,所以我们不用怎么改for循环体里的代码。

let 声明

现在你已经知道了var存在一些问题,这恰好说明了为什么用let语句来声明变量。 除了名字不同外,letvar的写法一致。

let hello = "Hello!";

主要的区别不在语法上,而是语义,我们接下来会深入研究。

块作用域

当用let声明一个变量,它使用的是词法作用域或块作用域。 不同于使用var声明的变量那样可以在包含它们的函数外访问, 块作用域变量在包含它们的块或for循环之外是不能访问的。

function f(input: boolean) {
    let a = 100;

    if (input) {
        // Still okay to reference 'a'
        let b = a + 1;
        return b;
    }

    // Error: 'b' doesn't exist here
    return b;
}

这里我们定义了2个变量aba的作用域是f函数体内, 而b的作用域是if语句块里。

在catch语句里声明的变量也具有同样的作用域规则。

try {
    throw "oh no!";
}
catch (e) {
    console.log("Oh well.");
}

// Error: 'e' doesn't exist here
console.log(e);

拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终「存在」于它们的作用域里, 但在直到声明它的代码之前的区域都属于暂时性死区。 它只是用来说明我们不能在let语句之前访问它们,幸运的是TypeScript可以告诉我们这些信息。

a++; // illegal to use 'a' before it's declared;
let a;

注意一点,我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。 如果生成代码目标为ES2015, 现代的运行时会抛出一个错误;然而,现今TypeScript是不会报错的。

function foo() {
    // okay to capture 'a'
    return a;
}

// 不能在'a'被声明前调用'foo'
// 运行时应该抛出错误
foo();

let a;

关于暂时性死区的更多信息,查看这里:

Mozilla Developer Network

重定义及屏蔽

我们提过使用var声明时,它不在乎你声明多少次;你只会得到1个。

function f(x) {
    var x;
    var x;

    if (true) {
        var x;
    }
}

在上面的例子里,所有x的声明实际上都引用一个相同的x,并且这是完全有效的代码。 这经常会成为bug的来源。 好的是,let声明就不会这么宽松了。

let x = 10;
let x = 20; // 错误,不能在1个作用域里多次声明`x`

并不是要求两个均是块级作用域的声明TypeScript才会给出一个错误的警告。

function f(x) {
    let x = 100; // error: interferes with parameter declaration
}

function g() {
    let x = 100;
    var x = 100; // error: can't have both declarations of 'x'
}

并不是说块级作用域变量不能用函数作用域变量来声明。 而是块级作用域变量需要在明显不同的块里声明。

function f(condition, x) {
    if (condition) {
        let x = 100;
        return x;
    }

    return x;
}

f(false, 0);  // returns 0
f(true , 0);  // returns 100

在一个嵌套作用域里引入一个新名字的行为称做屏蔽。 它是一把双刃剑, 它可能会不小心地引入新问题,同时也可能会解决一些错误。

const 声明

const声明是声明变量的另一种方式。

const numLivesForCat = 9;

它们引用的值是不可变的。

const numLivesForCat = 9;
const kitty = {
    name: "Aurora",
    numLives: numLivesForCat,
}

// Error
kitty = {
    name: "Danielle",
    numLives: numLivesForCat
};

// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;

除非你使用特殊的方法去避免,实际上const变量的内部状态是可修改的。 幸运的是, TypeScript允许你将对象的成员设置成只读的。 接口一章有详细说明。

let vs. const

现在我们有两种作用域相似的声明方式,我们自然会问到底应该使用哪个。 与大多数泛泛的问题一样,答案是:依情况而定。

使用最小特权原则,所有变量除了你计划去修改的都应该使用const。 基本原则就是如果一个变量不需要对它写入, 那么其它使用这些代码的人也不能够写入它们, 并且要思考为什么会需要对这些变量重新赋值。 使用const也可以让我们更容易的推测数据的流动。

另一方面,用户很喜欢let的简洁性。 这个手册大部分地方都使用了let

解构

Another TypeScript已经可以解析其它 ECMAScript 2015 特性了。 完整列表请参见

the article on the Mozilla Developer Network

解构数组

最简单的解构莫过于数组的解构赋值了:

let input = [1, 2];
let [first, second] = input;

console.log(first ); // outputs 1
console.log(second); // outputs 2

解构作用于已声明的变量会更好:

// swap variables
[first, second] = [second, first];

可以在数组里使用...语法创建剩余变量:

let [first, ...rest] = [1, 2, 3, 4];

console.log(first); // outputs 1
console.log(rest ); // outputs [ 2, 3, 4 ]

当然,由于是JavaScript, 你可以忽略你不关心的尾随元素:

let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1

或其它元素:

let [, second, , fourth] = [1, 2, 3, 4];

对象解构

你也可以解构对象:

let o = {
    a: "foo",
    b: 12,
    c: "bar"
};
let { a, b } = o;

这通过o.a and o.b创建了ab。 注意,如果你不需要c你可以忽略它。

就像数组解构,你可以用没有声明的赋值:

({ a, b } = { a: "baz", b: 101 });

注意,我们需要用括号将它括起来, 因为Javascript通常会将以{起始的语句解析为一个块。

你可以在对象里使用...语法创建剩余变量:

let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;

属性重命名

你也可以给属性以不同的名字:

let { a: newName1, b: newName2 } = o;

这里的语法开始变得混乱。 你可以将a: newName1读做「a作为newName1」。 方向是从左到右,好像你写成了以下样子:

let newName1 = o.a;
let newName2 = o.b;

令人困惑的是,这里的冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。

let {a, b}: {a: string, b: number} = o;

默认值

默认值可以让你在属性为undefined时使用缺省值:

function keepWholeObject(wholeObject: { a: string, b?: number }) {
    let { a, b = 1001 } = wholeObject;
}

现在,即使bundefinedkeepWholeObject函数的变量wholeObject的属性ab都会有值。

函数声明

解构也能用于函数声明。 看以下简单的情况:

type C = { a: string, b?: number }
function f({ a, b }: C): void {
    // ...
}

但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要在默认值之前设置其格式。

function f({ a, b } = { a: "", b: 0 }): void {
    // ...
}
f(); // ok, default to { a: "", b: 0 }
上面的代码是一个类型推断的例子,将在本手册后文介绍。

其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。 要知道C的定义有一个b可选属性:

function f({ a, b = 0 } = { a: "" }): void {
    // ...
}
f({ a: "yes" });   // ok, default b = 0
f();               // ok, default to {a: ""}, which then defaults b = 0
f({});             // error, 'a' is required if you supply an argument

要小心使用解构。 从前面的例子可以看出,就算是最简单的解构表达式也是难以理解的。 尤其当存在深层嵌套解构的时候,就算这时没有堆叠在一起的重命名,默认值和类型注解, 也是令人难以理解的。 解构表达式要尽量保持小而简单。 你自己也可以直接使用解构将会生成的赋值表达式。

展开

展开操作符正与解构相反。 它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。 例如:

let first    = [1, 2];
let second   = [3, 4];
let bothPlus = [0, ...first, ...second, 5];

这会令bothPlus的值为[0, 1, 2, 3, 4, 5]。 展开操作创建了firstsecond的一份浅拷贝。 它们不会被展开操作所改变。

你还可以展开对象:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search   = { ...defaults, food: "rich" };

search的值为{food: "rich", price: "$$", ambiance: "noisy"}。 对象的展开比数组的展开要复杂的多。 像数组展开一样,它是从左至右进行处理,但结果仍为对象。 这就意味着出现在展开对象后面的属性会覆盖前面的属性。 因此,如果我们修改上面的例子,在结尾处进行展开的话:

let defaults = {food: "spicy", price: "$$", ambiance: "noisy"};
let search   = {food: "rich", ...defaults};

那么,defaults里的food属性会重写food: "rich", 在这里这并不是我们想要的结果。

对象展开还有其它一些意想不到的限制。

首先,它仅包含对象自身的可枚举属性。 大体上是说当你展开一个对象实例时, 你会丢失其方法:

class C {
  p = 12;
  m() {
  }
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!

其次,TypeScript编译器不允许展开泛型函数上的类型参数。 这个特性会在TypeScript的未来版本中考虑实现。