一、基础类型
为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值
等。 TypeScript
支持与 JavaScript
几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。
1. 布尔值
最基本的数据类型就是简单的 true/false
值,在JavaScript
和 TypeScript
里叫做 boolean
let isDone: boolean = false;
2. 数字
和 JavaScript
一样,TypeScript
里的所有数字都是浮点数。 这些浮点数的类型是 number
。 除了支持十进制和十六进制字面量,TypeScript
还支持 ECMAScript 2015
中引入的二进制和八进制字面量。
let decLiteral: number = 6; let hexLiteral: number = 0xf00d; let binaryLiteral: number = 0b1010; let octalLiteral: number = 0o744;
3. 字符串
使用 string
表示文本数据类型。 和 JavaScript
一样,可以使用双引号(")或单引号(')表示字符串。
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.`; // 这与下面定义 sentence 的方式效果相同: let sentence: string = "Hello, my name is " + name + ".\n\n" + "I'll be " + (age + 1) + " years old next month.";
4. 数组
有两种方式可以定义数组。
// 第一种,可以在元素类型后面接上 `[]`,表示由此类型元素组成的一个数组: let list: number[] = [1, 2, 3]; // 第二种方式是使用数组泛型,Array<元素类型>: let list: Array<number> = [1, 2, 3];
5. 元组 tuple
元组类型允许表示一个 已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string
和 number
类型的元组。
// 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)类型
注:联合类型是高级主题,我们会在以后的章节里讨论它。
6. 枚举
enum
类型是对 JavaScript
标准数据类型的一个补充。 像 C#
等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。
enum Color {Red, Green, Blue} let c: Color = Color.Green; // 默认情况下,从 0 开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 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,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字: enum Color {Red = 1, Green, Blue} let colorName: string = Color[2]; console.log(colorName); // 显示 'Green' 因为上面代码里它的值是 2
7. 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;
8. void
某种程度上来说,void
类型像是与 any
类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void
:
function warnUser(): void { console.log("This is my warning message"); } // 声明一个 void 类型的变量没有什么大用,因为你只能为它赋予 undefined 和 null: let unusable: void = undefined;
9. null 和 undefined
TypeScript
里,undefined
和 null
两者各自有自己的类型分别叫做 undefined
和 null
。 和 void
相似,它们的本身的类型用处不是很大:
// Not much else we can assign to these variables! let u: undefined = undefined; let n: null = null;
默认情况下 null
和 undefined
是所有类型的子类型。 就是说你可以把 null
和 undefined
赋值给 number
类型的变量。
然而,当你指定了 --strictNullChecks
标记,null
和 undefined
只能赋值给 void
和它们各自。 这能避免 很多常见的问题。 也许在某处你想传入一个 string
或 null
或 undefined
,此时,你可以使用联合类型 string | null | undefined
。
注意:我们鼓励尽可能地使用--strictNullChecks
10. never
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) { } }
11. object
object
表示非原始类型,也就是除 number,string,boolean,symbol,null,undefined
之外的类型。
使用 object
类型,就可以更好的表示像 Object.create
这样的 API
。例如:
declare function create(o: object | null): void; create({ prop: 0 }); // OK create(null); // OK create(42); // Error create("string"); // Error create(false); // Error create(undefined); // Error
二、类型断言
有时候你会遇到这样的情况,你会比 TypeScript
更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript
会假设你,程序员,已经进行了必须的检查。
类型断言有两种形式。
// 1. “尖括号”语法: let someValue: any = "this is a string"; let strLength: number = (<string>someValue).length; // 2. as 语法: let someValue: any = "this is a string"; let strLength: number = (someValue as string).length;
两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在 TypeScript
里使用 JSX
时,只有 as
语法断言是被允许的。
三、变量声明
let
和 const
是 JavaScript
里相对较新的变量声明方式。 像我们之前提到过的, let
在很多方面与 var
是相似的,但是可以帮助大家避免在 JavaScript
里常见一些问题。 const
是对 let
的一个增强,它能阻止对一个变量再次赋值。
因为 TypeScript
是 JavaScript
的超集,所以它本身就支持 let
和 const
。 下面我们会详细说明这些新的声明方式以及为什么推荐使用它们来代替 var
。
1. let 声明
除了名字不同外, let
与 var
的写法一致。
let hello = "Hello!";
主要的区别不在语法上,而是语义,我们接下来会深入研究。
1> 块作用域
当用 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
个变量 a
和 b
。 a
的作用域是 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
.
2> 重定义及屏蔽
我们提过使用 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
在一个嵌套作用域里引入一个新名字的行为称做 屏蔽。 它是一把双刃剑,它可能会不小心地引入新问题,同时也可能会解决一些错误。 例如,假设我们现在用 let
重写之前的 sumMatrix
函数。
function sumMatrix(matrix: number[][]) { let sum = 0; for (let i = 0; i < matrix.length; i++) { var currentRow = matrix[i]; for (let i = 0; i < currentRow.length; i++) { sum += currentRow[i]; } } return sum; }
这个版本的循环能得到正确的结果,因为内层循环的 i
可以屏蔽掉外层循环的 i
。
通常来讲应该避免使用屏蔽,因为我们需要写出清晰的代码。 同时也有些场景适合利用它,你需要好好打算一下。
3> 块级作用域变量的获取
在我们最初谈及获取用 var
声明的变量时,我们简略地探究了一下在获取到了变量之后它的行为是怎样的。 直观地讲,每次进入一个作用域时,它创建了一个变量的 环境。 就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。
function theCityThatAlwaysSleeps() { let getCity; if (true) { let city = "Seattle"; getCity = function() { return city; } } return getCity(); }
因为我们已经在 city
的环境里获取到了 city
,所以就算 if
语句执行结束后我们仍然可以访问它。
回想一下前面 setTimeout
的例子,我们最后需要使用立即执行的函数表达式来获取每次 for
循环迭代里的状态。 实际上,我们做的是为获取到的变量创建了一个新的变量环境。 这样做挺痛苦的,但是幸运的是,你不必在 TypeScript
里这样做了。
当 let
声明出现在循环体里时拥有完全不同的行为。 不仅是在循环里引入了一个新的变量环境,而是针对 每次迭代 都会创建这样一个新作用域。 这就是我们在使用立即执行的函数表达式时做的事,所以在 setTimeout
例子里我们仅使用 let
声明就可以了。
for (let i = 0; i < 10 ; i++) { setTimeout(function() {console.log(i); }, 100 * i); } //会输出与预料一致的结果: 0 1 2 3 4 5 6 7 8 9
2. const 声明
const
声明是声明变量的另一种方式。
const numLivesForCat = 9;
它们与 let
声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与 let
相同的作用域规则,但是不能对它们重新赋值。
这很好理解,它们引用的值是不可变的。
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
允许你将对象的成员设置成只读的。 接口一章有详细说明。
3. let vs. const
现在我们有两种作用域相似的声明方式,我们自然会问到底应该使用哪个。 与大多数泛泛的问题一样,答案是:依情况而定。
使用 最小特权原则,所有变量除了你计划去修改的都应该使用 const
。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。 使用 const
也可以让我们更容易的推测数据的流动。
总结(12 种基本数据类型)
-
实用类型
-
number
-
string
-
boolean
-
-
空类型
-
undefined
:其他类型的子类型 -
null
:其他类型的子类型 -
void
-
never
:其他类型的子类型
-
-
任意类型
-
any
-
-
复合类型
-
enum
:编号;编号->
字符串 -
数组
-
tuple
:指定元素类型和个数的数组 -
object
-