基本类型
为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。 TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。
1.布尔值
最基本的数据类型就是简单的true/false,在JavaScript和TypeScript里叫做boolean
1 | let isDone: boolean = false; |
2.数字
和JavaScript一样,TypeScript 里的所有数字都是浮点数。这些浮点数的类型是 number
。 除了支持十进制和十六进制字面量。
1 | let decLiteral: number = 6; |
3.字符串
JavaScript的另一项基本操作是处理网页或服务器端的文本数据。和其他语言一样使用string表示文本数据类型。可以使用双引号( “ )或单引号( ‘ )表示字符串。
1 | let name: string = "bob"; |
还可以使用模板字符串,它可以定义多行文本和内嵌表达式。这种字符串是被反引号包围( ` ),并且以${ expr }这种形式嵌入表达式。
1 | let name: string = `Gene`; |
与下面定义sentence
的方式效果相同:
1 | let sentence: string = "Hello, my name is " + name + ".\n\n" + |
4.数组
TypeScript像JavaScript一样可以操作数组元素。有两种方式可以定义数组。
(1) 在元素类型后面接上 [ ]
,表示此类型元素组成的一个数组
1 | let list: number[] = [1, 2, 3]; |
(2) 使用数组泛型,Array<元素类型>
:
1 | let list: Array<number> = [1, 2, 3] |
5.元组 Tuple
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。比如,可以定义一对值分别为string
和number
类型的元组。
1 | // Declare a tuple type |
当访问一个已知索引的元素,会得到正确的类型:
1 | console.log(x[0].substr(1)); // OK |
当访问一个越界的元素,会使用联合类型替代:
1 | x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型 |
6.枚举
enum
类型是对JavaScript标准数据类型的一个补充。使用枚举类型可以为一组数值赋予友好的名字。
1 | enum Color{Red, Green, Blue} |
默认情况下,从0开始为元素编号。也可以手动指定成员的数值。
1 | enum Color{Red = 1, Green, Blue} |
此时元素编号从1开始。
也可以全部采用手动赋值:
1 | enum Color{Red = 1, Green = 2, Blue = 3} |
枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:
1 | enum Color {Red = 1, Green, Blue} |
7. Any
有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any
类型来标记这些变量:
1 | let notSure: any = 4; |
在对现有代码进行改写的时候,any
类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 你可能认为 Object
有相似的作用,就像它在其它语言中那样。 但是 Object
类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:
1 | let notSure: any = 4; |
当你只知道一部分数据的类型时,any
类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:
1 | let list: any[] = [1, true, "free"]; |
8. Void
某种程度上来说,void
类型像是与any
类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void
:
1 | function warnUser(): void { |
声明一个void
类型的变量没有什么大用,因为你只能为它赋予undefined
和null
:
1 | let unusable: void = undefined; |
9. Null 和 Undefined
TypeScript 里,undefined
和null
两者各自有自己的类型分别叫做undefined
和null
。 和 void
相似,它们的本身的类型用处不是很大:
1 | // Not much else we can assign to these variables! |
默认情况下null
和undefined
是所有类型的子类型。 就是说你可以把 null
和undefined
赋值给number
类型的变量。
当指定了--strictNullChecks
标记,null
和undefined
只能赋值给void
和它们各自。 这能避免 很多常见的问题。 也许在某处你想传入一个 string
或null
或undefined
,你可以使用联合类型string | null | undefined
。 再次说明,稍后我们会介绍联合类型。
10. Never
never
类型表示的是那些永不存在的值的类型。 例如, never
类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never
类型,当它们被永不为真的类型保护所约束时。
never
类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never
的子类型或可以赋值给never
类型(除了never
本身之外)。 即使 any
也不可以赋值给never
。
1 | // 返回never的函数必须存在无法达到的终点 |
11. Object
object
表示非原始类型,也就是除number
,string
,boolean
,symbol
,null
或undefined
之外的类型。
使用object
类型,就可以更好的表示像Object.create
这样的 API 。
1 | declare function create(o: object | null): void; |
12.类型断言
有时候你会比 TypeScript 更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript 会假设你,程序员,已经进行了必须的检查。
类型断言有两种形式。
(1) “尖括号形式”
1 | let someValue: any = "this is a string"; |
(2) as
语法
1 | let someValue: any = "this is a string"; |
两种形式是等价的。但当你在 TypeScript 里使用 JSX 时,只有 as
语法断言是被允许的。
变量声明
let
和const
是JavaScript里相对较新的变量声明方式。 let
在很多方面与var
是相似的,但是可以避免在JavaScript里常见的一些问题。 const
是对let
的一个增强,它能阻止对一个变量再次赋值。
1. var
声明
1 | var a = 10; |
也可以在函数内部定义变量:
1 | function f() { |
也可以在其它函数内部访问相同的变量。
1 | function f() { |
g
可以获取到f
函数里定义的a
变量。 每当 g
被调用时,它都可以访问到f
里的a
变量。 即使当 g
在f
已经执行完后才被调用,它仍然可以访问及修改a
。
1 | function f() { |
作用域规则
var
声明的作用域。
1 | function f(shouldInitialize: boolean) { |
变量 x
是定义在*if
语句里面,但是我们却可以在语句的外面访问它。 这是因为 var
声明可以在包含它的函数,模块,命名空间或全局作用域内部任何位置被访问,包含它的代码块对此没有什么影响。 有些人称此为 var
作用域或函数作用域*。 函数参数也使用函数作用域。
这些作用域规则可能会引发一些错误。 其中之一就是,多次声明同一个变量并不会报错:
1 | function sumMatrix(matrix: number[][]) { |
问题:里层的for
循环会覆盖变量i
,因为所有i
都引用相同的函数作用域内的变量。这些问题可能在代码审查时漏掉,引发无穷的麻烦。
捕获变量怪异之处
1 | for (var i = 0; i < 10; i++) { |
setTimeout
会在若干毫秒的延时后执行一个函数
返回的结果是:
1 | 10 |
传给
setTimeout
的每一个函数表达式实际上都引用了相同作用域里的同一个i
。
2.let
声明
除了名字不同外, let
与var
的写法一致。
1 | let hello = "Hello!"; |
块作用域
当用let
声明一个变量,它使用的是词法作用域或块作用域。 不同于使用 var
声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或for
循环之外是不能访问的。
1 | function f(input: boolean) { |
这里定义了2个变量a
和b
。 a
的作用域是f
函数体内,而b
的作用域是if
语句块里。
在catch
语句里声明的变量也具有同样的作用域规则。
1 | try { |
拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于 暂时性死区。
1 | a++; // illegal to use 'a' before it's declared; |
我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。
1 | function foo() { |
重定义及屏蔽
使用var
声明时,它不在乎你声明多少次;你只会得到1个。
1 | function f(x) { |
在上面的例子里,所有x
的声明实际上都引用一个相同的x
,并且这是完全有效的代码。 这经常会成为bug的来源。 但 let
声明不允许重复声明。
1 | let x = 10; |
并不是要求两个均是块级作用域的声明 TypeScript 才会给出一个错误的警告。
1 | function f(x) { |
块级作用域变量需要在不同的块里声明。
1 | function f(condition, x) { |
块级作用域变量的获取
每次进入一个作用域时,它创建了一个变量的 环境。 就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。
1 | let unusable: void = undefined;function theCityThatAlwaysSleeps() { |
因为我们已经在city
的环境里获取到了city
,所以就算if
语句执行结束后我们仍然可以访问它。
当let
声明出现在循环体里时拥有完全不同的行为。 不仅是在循环里引入了一个新的变量环境,而是针对 每次迭代都会创建这样一个新作用域。
1 | for (let i = 0; i < 10 ; i++) { |
此时的输出结果是:
1 | 0 |
3.const
声明
const
声明是声明变量的另一种方式。
1 | const numLivesForCat = 9; |
与let
声明相似,但它们被赋值后不能再改变。它们拥有与 let
相同的作用域规则,但是不能对它们重新赋值。
1 | const numLivesForCat = 9; |
const
变量的内部状态是可修改的。
1 | let unusable: void = undefined; |
4.解构
数组解构
1 | let input = [1, 2]; |
可以在数组里使用...
语法进行解构:
1 | let [first, ...rest] = [1, 2, 3, 4]; |
对象解构
1 | let o = { |
可以在对象里使用...
语法创建剩余变量:
1 | let { a, ...passthrough } = o; |
属性重命名
可以给属性以不同的名字:
1 | let { a: newName1, b: newName2 } = o; |
可以将 a: newName1
读做 “a
作为 newName1
“。
1 | let newName1 = o.a; |
这里的冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。
1 | let {a, b}: {a: string, b: number} = o; |
默认值
默认值可以让你在属性为 undefined 时使用缺省值:
1 | function keepWholeObject(wholeObject: { a: string, b?: number }) { |
即使 b
为 undefined , keepWholeObject
函数的变量 wholeObject
的属性 a
和 b
都会有值。
函数声明
解构也能用于函数声明。
1 | type C = { a: string, b?: number } |
展开
展开操作符正与解构相反。 它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。
1 | let first = [1, 2]; |
这会令bothPlus
的值为[0, 1, 2, 3, 4, 5]
。 展开操作创建了 first
和second
的一份浅拷贝。 它们不会被展开操作所改变。
也可以展开对象:
1 | let defaults = { food: "spicy", price: "$$", ambiance: "noisy" }; |
search
的值为{ food: "rich", price: "$$", ambiance: "noisy" }
。
1 | let defaults = { food: "spicy", price: "$$", ambiance: "noisy" }; |
defaults里的
food属性会重写
food: “rich”。
Symbols
symbol
是一种新的原生类型,就像number
和string
一样。
symbol
类型的值是通过Symbol
构造函数创建的。
1 | let sym1 = Symbol(); |
Symbols是不可改变且唯一的。
1 | let sym2 = Symbol("key"); |
像字符串一样,symbols也可以被用做对象属性的键。
1 | let sym = Symbol(); |
Symbols也可以与计算出的属性名声明相结合来声明对象的属性和类成员。
1 | const getClassNameSymbol = Symbol(); |
除了用户定义的symbols,还有一些已经众所周知的内置symbols。 内置symbols用来表示语言内部的行为。
symbols的列表:
Symbol.hasInstance
方法,会被instanceof
运算符调用。构造器对象用来识别一个对象是否是其实例。
Symbol.isConcatSpreadable
布尔值,表示当在一个对象上调用Array.prototype.concat
时,这个对象的数组元素是否可展开。
Symbol.iterator
方法,被for-of
语句调用。返回对象的默认迭代器。
Symbol.match
方法,被String.prototype.match
调用。正则表达式用来匹配字符串。
Symbol.replace
方法,被String.prototype.replace
调用。正则表达式用来替换字符串中匹配的子串。
Symbol.search
方法,被String.prototype.search
调用。正则表达式返回被匹配部分在字符串中的索引。
Symbol.species
函数值,为一个构造函数。用来创建派生对象。
Symbol.split
方法,被String.prototype.split
调用。正则表达式来用分割字符串。
Symbol.toPrimitive
方法,被ToPrimitive
抽象操作调用。把对象转换为相应的原始值。
Symbol.toStringTag
方法,被内置方法Object.prototype.toString
调用。返回创建对象时默认的字符串描述。
Symbol.unscopables
对象,它自己拥有的属性会被with
作用域排除在外。