TypeScript类型基础
类型注解
使用类型注解来明确标识类型。类型注解的语法由一个冒号“:”和某种具体类型“Type”组成;
:Type
TypeScript中的类型注解总是放在被修饰的实体之后;
TypeScript中的类型注解是可选的,编译器在大部分情况下都能够自动推断出表达式的类型。
const greeting: string = 'Hello, World!';
const greeting = 'Hello, World!';
类型检查
类型检查是验证程序中类型约束是否正确的过程。类型检查既可以在程序编译时进行,即静态类型检查;也可以在程序运行时进行,即动态类型检查。TypeScript支持静态类型检查,JavaScript支持动态类型检查。
为了满足不同用户的需求,TypeScript提供了两种静态类型检查模式:
- 非严格类型检查(默认方式)
- 严格类型检查
非严格类型检查
非严格类型检查是TypeScript默认的类型检查模式。在该模式下,类型检查的规则相对宽松。
例如,在非严格类型检查模式下不会对undefined值和null值做过多限制,允许将undefined值和null值赋值给string类型的变量。
当进行JavaScript代码到TypeScript代码的迁移工作时,非严格类型检查是一个不错的选择,因为它能够让我们快速地完成迁移工作。
严格类型检查
该模式下的类型检查比较激进,会尽可能地发现代码中的错误。
例如,在严格类型检查模式下不允许将undefined值和null值赋值给string类型的变量。
启用严格类型检查模式能够最大限度地利用TypeScript静态类型检查带来的益处。从长远来讲,使用严格类型检查模式对提高代码质量更加有利,因此建议在新的工程中启用严格类型检查。
从长远来讲,使用严格类型检查模式对提高代码质量更加有利,因此建议在新的工程中启用严格类型检查。
TypeScript提供了若干个与严格类型检查相关的编译选项,例如“--strictNullChecks”和“--noImplicitAny”等。关于严格类型检查编译选项的详细介绍请参考 “编译选项”。
原始类型
JavaScript语言中的每种原始类型都有与之对应的TypeScript类型。除此之外,TypeScript还对原始类型进行了细化与扩展,增加了枚举类型和字面量类型等。
TypeScript中的原始类型包含以下几种:
- boolean
- string
- number
- bigint
- symbol
- undefined
- null
- void
- 枚举类型
- 字面量类型
boolean
TypeScript中的boolean类型对应于JavaScript中的Boolean原始类型。该类型能够表示两个逻辑值:true和false。
// boolean类型使用boolean关键字来表示
const yes: boolean = true
const no: boolean = false;
string
TypeScript中的string类型对应于JavaScript中的String原始类型。
该类型能够表示采用Unicode UTF-16编码格式存储的字符序列。
string类型使用string关键字表示。我们通常使用字符串字面量或模板字面量来创建string类型的值。
const foo: string = 'foo';
const bar: string = `bar, ${foo}`;
number
TypeScript中的number类型对应于JavaScript中的Number原始类型。
该类型能够表示采用双精度64位二进制浮点数格式存储的数字。
// 二进制数
const bin: number = 0b1010;
// 八进制数
const oct: number = 0o744;
// 十进制数
const integer: number = 10;
const float: number = 3.14;
// 十六进制数
const hex: number = 0xffffff;
bigint
TypeScript中的bigint类型对应于JavaScript中的BigInt原始类型。
该类型能够表示任意精度的整数,但也仅能表示整数。
bigint采用了特殊的对象数据结构来表示和存储一个整数。
// bigint类型使用bigint关键字来表示。
// 二进制整数
const bin: bigint = 0b1010n;
// 八进制整数
const oct: bigint = 0o744n;
// 十进制整数
const integer: bigint = 10n;
// 十六进制整数
const hex: bigint = 0xffffffn;
symbol与unique symbol
TypeScript中的symbol类型对应于JavaScript中的Symbol原始类型。
该类型能够表示任意的Symbol值。
// symbol类型使用symbol关键字来表示
// 自定义Symbol
const key: symbol = Symbol();
// Well-Known Symbol
const symbolHasInstance: symbol = Symbol.hasInstance;
字面量能够表示一个固定值。例如,数字字面量“3”表示固定数值“3”;字符串字面量“'up'”表示固定字符串
“'up'”。symbol类型不同于其他原始类型,它不存在字面量形式。symbol类型的值只能通过“Symbol()”和“Symbol.for()”函数来创建或直接引用某个“Well-Known Symbol”值。
const s0: symbol = Symbol();
const s1: symbol = Symbol.for('foo');
const s2: symbol = Symbol.hasInstance;
const s3: symbol = s0;
为了能够将一个Symbol值视作表示固定值的字面量,TypeScript引入了“unique symbol”类型。“unique symbol”类型使用“unique symbol”关键字来表示。
const s0: unique symbol = Symbol();
const s1: unique symbol = Symbol.for('s1');
“unique symbol”类型的主要用途是用作接口、类等类型中的可计算属性名。因为如果使用可计算属性名在接口中添加了一个类型成员,那么必须保证该类型成员的名字是固定的,否则接口定义将失去意义。
下例中,允许将“unique symbol”类型的常量x作为接口的类型成员,而symbol类型的常量y不能作为接口的类型成员,因为symbol类型不止包含一个可能值:
const x: unique symbol = Symbol();
const y: symbol = Symbol();
interface Foo {
[x]: string; // 正确
[y]: string;
// ~~~
// 错误:接口中的计算属性名称必须引用类型为字面量类型
// 或'unique symbol'的表达式
}
关于"unique symbol"
实际上,“unique symbol”类型的设计初衷是作为一种变通方法,让一个Symbol值具有字面量的性质,即仅表示一个固定的值。“unique symbol”类型没有改变Symbol值没有字面量表示形式的事实。为了能够将某个Symbol值视作表示固定值的字面量,TypeScript对“unique symbol”类型和Symbol值的使用施加了限制。
TypeScript选择将一个Symbol值与声明它的标识符绑定在一起,并通过绑定了该Symbol值的标识符来表示“Symbol字面量”。这种设计的前提是要确保Symbol值与标识符之间的绑定关系是不可变的。因此,TypeScript中只允许使用const声明或readonly属性声明来定义“unique symbol”类型的值。
// 必须使用const声明
const a: unique symbol = Symbol();
interface WithUniqueSymbol {
// 必须使用readonly修饰符
readonly b: unique symbol;
}
class C {
// 必须使用static和readonly修饰符
static readonly c: unique symbol = Symbol();
}
此例第1行,常量a的初始值为Symbol值,其类型为“uniquesymbol”类型。在标识符a与其初始值Symbol值之间形成了绑定关系,并且该关系是不可变的。这是因为常量的值是固定的,不允许再被赋予其他值。标识符a能够固定表示该Symbol值,标识符a的角色相当于该Symbol值的字面量形式。
如果使用let或var声明定义“unique symbol”类型的变量,那么将产生错误,因为标识符与Symbol值之间的绑定是可变的。
let a: unique symbol = Symbol();
// ~
// 错误:'unique symbol' 类型的变量必须使用'const'
var b: unique symbol = Symbol();
// ~
// 错误:'unique symbol' 类型的变量必须使用'const'
“unique symbol”类型的值只允许使用“Symbol()”函数或“Symbol.for()”方法的返回值进行初始化,因为只有这样才能够“确保”引用了唯一的Symbol值。
const a: unique symbol = Symbol();
const b: unique symbol = Symbol('desc');
const c: unique symbol = a;
// ~
// 错误:a的类型与c的类型不兼容
const d: unique symbol = b;
// ~
// 错误:b的类型与d的类型不兼容
但是,我们知道使用相同的参数调用“Symbol.for()”方法实际上返回的是相同的Symbol值。因此,可能出现多个“unique symbol”类型的值实际上是同一个Symbol值的情况。由于设计上的局限性,TypeScript目前无法识别出这种情况,因此不会产生编译错误,开发者必须要留意这种特殊情况。
const a: unique symbol = Symbol.for('same');
const b: unique symbol = Symbol.for('same');
此例中,编译器会认为a和b是两个不同的Symbol值,而实际上两者是相同的。
在设计上,每一个“unique symbol”类型都是一种独立的类型。在不同的“unique symbol”类型之间不允许相互赋值;在比较两个“unique symbol”类型的值时,也将永远返回false。
const a: unique symbol = Symbol();
const b: unique symbol = Symbol();
if (a === b) {
// ~~~~~~~
// 该条件永远为false
console.log('unreachable code');
}
由于“unique symbol”类型是 symbol类型的子类型,因此可以将“unique symbol”类型的值赋值给symbol类型。
如果程序中未使用类型注解来明确定义是symbol类型还是“unique symbol”类型,那么TypeScript会自动地推断类型。
// a和b均为'symbol'类型,因为没有使用const声明
let a = Symbol();
let b = Symbol.for('');
// c和d均为'unique symbol'类型
const c = Symbol();
const d = Symbol.for('');
// e和f均为'symbol'类型,没有使用Symbol()或Symbol.for()初始化
const e = a;
const f = a;
Nullable
TypeScript中的Nullable类型指的是值可以为undefined或null的类型。
JavaScript中有两个比较特殊的原始类型,即Undefined类型和Null类型。两者分别仅包含一个原始值,即undefined值和null值,它们通常用来表示某个值还未进行初始化。
在TypeScript早期的版本中,没有提供与JavaScript中Undefined类型和Null类型相对应的类型。TypeScript允许将undefined值和null值赋值给任何其他类型。虽然在TypeScript语言的内部实现中确实存在这两种原始类型,但是之前没有将它们开放给开发者使用。
TypeScript 2.0版本的一个改变就是增加了undefined类型和null类型供开发者使用。
现在,在TypeScript程序中能够明确地指定某个值的类型是否为undefined类型或null类型。TypeScript编译器也能够对代码进行更加细致的检查以找出程序中潜在的错误。
undefined
undefined类型只包含一个可能值,即undefined值。undefined类型使用undefined关键字标识。
const foo: undefined = undefined;
null
null类型只包含一个可能值,即null值。null类型使用null关键字标识。
const foo: null = null;
--strictNullChecks
TypeScript 2.0还增加了一个新的编译选项“--strictNullChecks”,即严格的null检查模式。虽然该编译选项的名
字中只提及了null,但实际上它同时作用于undefined类型和null类型的类型检查。
在默认情况下,“--strictNullChecks”编译选项没有被启用。这时候,除尾端类型外的所有类型都是Nullable类型。也就是说,除尾端类型外所有类型都能够接受undefined值和null值。
在没有启用“--strictNullChecks”编译选项时,允许将undefined值和null值赋值给string类型等其他类型。
/**
* --strictNullChecks=false
*/
let m1: boolean = undefined;
let m2: string = undefined;
let m3: number = undefined;
let m4: bigint = undefined;
let m5: symbol = undefined;
let m6: undefined = undefined;
let m7: null = undefined;
let n1: boolean = null;
let n2: string = null;
let n3: number = null;
let n4: bigint = null;
let n5: symbol = null;
let n6: undefined = null;
let n7: null = null;
该模式存在一个明显的问题,就是无法检查出空引用的错误。例如,已知某一个变量的类型是string,于是通过访问其length属性来获取该变量表示的字符串的长度。但如果string类型的变量值可以为undefined或null,那么这段代码在运行时将产生错误。
/**
* --strictNullChecks=false
*/
let foo: string = undefined; // 正确,可以通过类型检查
foo.length; // 在运行时,将产生类型错误
// 运行结果:
// Error: TypeError: Cannot read property 'length'
// of undefined
此例中,将undefined值赋值给string类型的变量foo时不会产生编译错误。但是,在运行时尝试读取undefined值的length属性将产生类型错误。这个问题可以通过启用“--strictNullChecks”编译选项来避免。
当启用了“--strictNullChecks”编译选项时,undefined值和null值不再能够赋值给不相关的类型。例如,undefined值和null值不允许赋值给string类型。在该模式下,undefined值只能够赋值给undefined类型;同理,null值也只能赋值给null类型。更严谨的说法(赋值规则)是:undefined值和null值允许赋值给顶端类型(unkown 和 any ),同时undefined值也允许赋值给void类型。
当启用了“--strictNullChecks”编译选项时,undefined类型和null类型是不同的类型,它们必须被区分对待,不能互换使用。
/**
* --strictNullChecks=true
*/
const foo: undefined = null;
// ~~~
// 编译错误!类型 'null' 不能赋值给类型 'undefined'
const bar: null = undefined;
// ~~~
// 编译错误!类型 'undefined' 不能赋值给类型 'null'
枚举类型
枚举类型由零个或多个枚举成员构成,每个枚举成员都是一个命名的常量。
在TypeScript中,枚举类型是一种原始类型,它通过enum关键字来定义
按照枚举成员的类型可以将枚举类型划分为以下三类:
- 数值型枚举
- 字符串枚举
- 异构型枚举
数值型枚举
数值型枚举是最常用的枚举类型,是number类型的子类型,它由一组命名的数值常量构成。
enum Direction {
Up,
Down,
Left,
Right
}
const direction: Direction = Direction.Up;
此例中,我们使用enum关键字定义了枚举类型Direction,它包含了四个枚举成员Up、Down、Left和Right。在使用枚举成员时,可以像访问对象属性一样访问枚举成员。
每个数值型枚举成员都表示一个具体的数字。如果在定义枚举时没有设置枚举成员的值,那么TypeScript将自动计算枚举成员的值。根据TypeScript语言的规则,第一个枚举成员的值为0,其后每个枚举成员的值等于前一个枚举成员的值加1。
在定义数值型枚举时,可以为一个或多个枚举成员设置初始值。对于未指定初始值的枚举成员,其值为前一个枚举成员的值加1。
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
enum Direction {
Up = 1, // 1
Down, // 2
Left = 10, // 10
Right, // 11
}
数值型枚举是number类型的子类型,因此允许将数值型枚举类型赋值给number类型。例如,下例中常量direction为number类型,可以使用数值型枚举Direction来初始化direction常量。
enum Direction {
Up,
Down,
Left,
Right
}
const direction: number = Direction.Up;
需要注意的是,number类型也能够赋值给枚举类型,即使number类型的值不在枚举成员值的列表中也不会产生错误。
enum Direction {
Up,
Down,
Left,
Right,
}
const d1: Direction = 0; // Direction.Up
const d2: Direction = 10; // 不会产生错误
字符串枚举
字符串枚举与数值型枚举相似。在字符串枚举中,枚举成员的值为字符串。字符串枚举成员必须使用字符串字面量或另一个字符串枚举成员来初始化。字符串枚举成员没有自增长的行为。
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
U = Up,
D = Down,
L = Left,
R = Right,
}
字符串枚举是string类型的子类型,因此允许将字符串枚举类型赋值给string类型。
但是反过来,不允许将string类型赋值给字符串枚举类型,这一点与数值型枚举是不同的。
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
const direction: string = Direction.Up;
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
const direction: Direction = 'UP';
// ~~~~~~~~~
// 编译错误!类型 'UP' 不能赋值给类型 'Direction'
异构型枚举
TypeScript允许在一个枚举中同时定义数值型枚举成员和字符串枚举成员,我们将这种类型的枚举称作异构型枚举。
异构型枚举在实际代码中很少被使用,虽然在语法上允许定义异构型枚举,但是不推荐在代码中使用异构型枚举。我们可以尝试使用对象来代替异构型枚举。
enum Color {
Black = 0,
White = 'White',
}
在定义异构型枚举时,不允许使用计算的值作为枚举成员的初始值。
enum Color {
Black = 0 + 0,
// ~~~~~
// 编译错误!在带有字符串成员的枚举中不允许使用计算值
White = 'White',
}
在异构型枚举中,必须为紧跟在字符串枚举成员之后的数值型枚举成员指定一个初始值。
enum ColorA {
Black,
White = 'White',
}
enum ColorB {
White = 'White',
Black,
// ~~~~~
// 编译错误!枚举成员必须有一个初始值
}
枚举成员映射
不论是哪种类型的枚举,都可以通过枚举成员名去访问枚举成员值。
enum Bool {
False = 0,
True = 1,
}
Bool.False; // 0
Bool.True; // 1
对于数值型枚举,不但可以通过枚举成员名来获取枚举成员值,也可以反过来通过枚举成员值去获取枚举成员名。
下例中,通过枚举成员值“Bool.False”能够获取其对应的枚举成员名,即字符串“'False'”
enum Bool {
False = 0,
True = 1,
}
Bool[Bool.False]; // 'False'
Bool[Bool.True]; // 'True'
对于字符串枚举和异构型枚举,则不能够通过枚举成员值去获取枚举成员名。
常量枚举成员与计算枚举成员
每个枚举成员都有一个值,根据枚举成员值的定义可以将枚举成员划分为以下两类:
- 常量枚举成员
- 计算枚举成员
常量枚举成员
若枚举类型的第一个枚举成员没有定义初始值,那么该枚举成员是常量枚举成员并且初始值为0。
若枚举成员没有定义初始值并且与之紧邻的前一个枚举成员值是数值型常量,那么该枚举成员是常量枚举成员并且初始值为紧邻的前一个枚举成员值加1。如果紧邻的前一个枚举成员的值不是数值型常量,那么将产生错误。
enum Foo {
A, // 0
B, // 1
}
enum Bar {
C = 'C',
D, // 编译错误
}
若枚举成员的初始值是常量枚举表达式,那么该枚举成员是常量枚举成员。常量枚举表达式是TypeScript表达式的子集,它能够在编译阶段被求值。常量枚举表达式的具体规则如下:
- 常量枚举表达式可以是数字字面量、字符串字面量和不包含替换值的模板字面量。
- 常量枚举表达式可以是对前面定义的常量枚举成员的引用。
- 常量枚举表达式可以是用分组运算符包围起来的常量枚举表达式。
- 常量枚举表达式中可以使用一元运算符“+” “-” “~”,操作数必须为常量枚举表达式。
- 常量枚举表达式中可以使用二元运算符“+” “-” “*” “**” “/” “%” “<<” “>>” “>>>” “&” “|” “^”,两个操作数必须为常量枚举表达式。
enum Foo {
A = 0, // 数字字面量
B = 'B', // 字符串字面量
C = `C`, // 无替换值的模板字面量
D = A, // 引用前面定义的常量枚举成员
}
enum Bar {
A = -1, // 一元运算符
B = 1 + 2, // 二元运算符
C = (4 / 2) * 3, // 分组运算符(小括号)
}
字面量枚举成员是常量枚举成员的子集。字面量枚举成员是指满足下列条件之一的枚举成员,具体条件如下:
- 枚举成员没有定义初始值。
- 枚举成员的初始值为数字字面量、字符串字面量和不包含替换值的模板字面量。
- 枚举成员的初始值为对其他字面量枚举成员的引用。
enum Foo {
A,
B = 1,
C = -3,
D = 'foo',
E = `bar`,
F = A
}
计算枚举成员
除常量枚举成员之外的其他枚举成员都属于计算枚举成员。
使用示例
枚举表示一组有限元素的集合,并通过枚举成员名来引用集合中的元素。
有时候,程序中并不关注枚举成员值。在这种情况下,让TypeScript去自动计算枚举成员值是很方便的。
enum Direction {
Up,
Down,
Left,
Right,
}
function move(direction: Direction) {
switch (direction) {
case Direction.Up:
console.log('Up');
break;
case Direction.Down:
console.log('Down');
break;
case Direction.Left:
console.log('Left');
break;
case Direction.Right:
console.log('Right');
break;
}
}
move(Direction.Up); // 'Up'
move(Direction.Down); // 'Down'
程序不依赖枚举成员值时,能够降低代码耦合度,使程序易于扩展。例如,我们想给Direction枚举添加一个名为None的枚举成员来表示未知方向。
enum Direction {
None,
Up,
Down,
Left,
Right,
}
function move(direction: Direction) {
switch (direction) {
case Direction.None:
console.log('None');
break;
case Direction.Up:
console.log('Up');
break;
case Direction.Down:
console.log('Down');
break;
case Direction.Left:
console.log('Left');
break;
case Direction.Right:
console.log('Right');
break;
}
}
move(Direction.Up); // 'Up'
move(Direction.Down); // 'Down'
move(Direction.None); // 'None'
此例中,枚举成员Up、Down、Left和Right的值已经发生了改变,Up的值由0变为1,以此类推。由于move()函数的行为不直接依赖枚举成员的值,因此本次代码修改对move()函数的已有功能不产生任何影响。
但如果程序中赖了枚举成员的具体值,那么这次代码修改就会破坏现有的代码。
enum Direction {
None,
Up,
Down,
Left,
Right,
}
function move(direction: Direction) {
switch (direction) {
// 不会报错,但是逻辑错误,Direction.Up的值已经不是数字0
case 0:
console.log('Up');
break;
// 省略其他代码
}
}
联合枚举类型
当枚举类型中的所有成员都是字面量枚举成员时,该枚举类型成了联合枚举类型。
联合枚举成员类型
联合枚举类型中的枚举成员除了能够表示一个常量值外,还能够表示一种类型,即联合枚举成员类型。
下例中,Direction枚举是联合枚举类型,Direction枚举成员Up、Down、Left和Right既表示数值常量,也表示联合枚举成员类型:
enum Direction {
Up,
Down,
Left,
Right,
}
08 const up: Direction.Up = Direction.Up;
此例第8行,第一个“Direction.Up”表示联合枚举成员类型,第二个“Direction.Up”则表示数值常量0。
联合枚举成员类型是联合枚举类型的子类型,因此可以将联合枚举成员类型赋值给联合枚举类型。
enum Direction {
Up,
Down,
Left,
Right,
}
const up: Direction.Up = Direction.Up;
const direction: Direction = up;
此例中,常量up的类型是联合枚举成员类型“Direction.Up”,常量direction的类型是联合枚举类型Direction。由于“Direction.Up”类型是Direction类型的子类型,因此可以将常量up赋值给常量direction。
联合枚举类型
联合枚举类型是由所有联合枚举成员类型构成的联合类型。
enum Direction {
Up,
Down,
Left,
Right,
}
type UnionDirectionType =
| Direction.Up
| Direction.Down
| Direction.Left
| Direction.Right;
此例中,Direction枚举是联合枚举类型,它等同于联合类型UnionDirectionType,其中“|”符号是定义联合类型的语法。
由于联合枚举类型是由固定数量的联合枚举成员类型构成的联合类型,因此编译器能够利用该性质对代码进行类型检查。
enum Direction {
Up,
Down,
Left,
Right,
}
function f(direction: Direction) {
if (direction === Direction.Up) {
// Direction.Up
} else if (direction === Direction.Down) {
// Direction.Down
} else if (direction === Direction.Left) {
// Direction.Left
} else {
// 能够分析出此处的direction为Direction.Right
direction;
}
}
此例中,编译器能够分析出Direction联合枚举类型只包含四种可能的联合枚举成员类型。在“if-else”语句中,编译器能够根据控制流分析出最后的else分支中direction的类型为“Direction.Right”。
下面再来看另外一个例子。Foo联合枚举类型由两个联合枚举成员类型“Foo.A”和“Foo.B”构成。编译器能够检查出在第7行if条件判断语句中的条件表达式结果永远为true,因此将产生编译错误。
01 enum Foo {
02 A = 'A',
03 B = 'B',
04 }
05
06 function bar(foo: Foo) {
07 if (foo !== Foo.A || foo !== Foo.B) {
08 // ~~~~~~~~~~~~~
09 // 编译错误:该条件永远为'true'
10 }
11 }
下例中,由于Foo联合枚举类型等同于联合类型“Foo.A | Foo.B”,因此它是联合类型“'A' | 'B'”的子类型。
01 enum Foo {
02 A = 'A',
03 B = 'B',
04 }
05
06 enum Bar {
07 A = 'A',
08 }
09
10 enum Baz {
11 B = 'B',
12 C = 'C',
13 }
14
15 function f1(x: 'A' | 'B') {
16 console.log(x);
17 }
18
19 function f2(foo: Foo, bar: Bar, baz: Baz) {
20 f1(foo);
21 f1(bar);
22
23 f1(baz);
24 // ~~~
25 // 错误:类型 'Baz' 不能赋值给参数类型'A' | 'B'
26 }
此例第15行,f1函数接受“'A' | 'B'”联合类型的参数x。第20行,允许使用Foo枚举类型的参数foo调用函数f1,因为Foo枚举类型是“'A' | 'B'”类型的子类型。第21行,允许使用Bar枚举类型的参数bar调用函数f1,因为Bar枚举类型是'A'类型的子类型,显然也是“'A' | 'B'”类型的子类型。第23行,不允许使用Baz枚举类型的参数baz调用函数f1,因为Baz枚举类型是“'B' | 'C'”类型的子类型,显然与“'A' | 'B'”类型不兼容,所以会产生错误。
const枚举类型
枚举类型是TypeScript对JavaScript的扩展,JavaScript语言本身并不支持枚举类型。在编译时,TypeScript编译器会将枚举类型编译为JavaScript对象。
enum Direction {
Up,
Down,
Left,
Right,
}
const d: Direction = Direction.Up;
上面的代码编译后生成的JavaScript代码如下所示,为了支持枚举成员名与枚举成员值之间的正、反向映射关系,TypeScript还生成了一些额外的代码:
"use strict";
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
const d = Direction.Up;
有时候我们不会使用枚举成员值到枚举成员名的反向映射,因此没有必要生成额外的反向映射代码,只需要生成如下代码就能够满足需求:
01 "use strict";
02 var Direction;
03 (function (Direction) {
04 Direction["Up"] = 0;
05 Direction["Down"] = 1
06 Direction["Left"] = 2
07 Direction["Right"] = 3
08 })(Direction || (Direction = {}));
09
10 const d = Direction.Up;
更进一步讲,如果我们只关注第10行枚举类型的使用方式就会发现,完全不需要生成与Direction对象相关的代码,只需要将“Direction.Up”替换为它所表示的常量0即可。经过此番删减后的代码量将大幅减少,并且不会改变程序的运行结果,如下所示:
"use strict";
const d = 0;
const枚举类型具有相似的效果。const枚举类型将在编译阶段被完全删除,并且在使用了const枚举类型的地方会直接将const枚举成员的值内联到代码中。
const enum Directions {
Up,
Down,
Left,
Right,
}
const directions = [
Directions.Up,
Directions.Down,
Directions.Left,
Directions.Right,
];
//代码经过TypeScript编译器编译后生成的JavaScript代码
"use strict";
const directions = [
0 /* Up */,
1 /* Down */,
2 /* Left */,
3 /* Right */
];
为了便于代码调试和保持代码的可读性,TypeScript编译器在内联了const枚举成员的位置还额外添加了注释,注释的内容为枚举成员的名字。
字面量类型
TypeScript支持将字面量作为类型使用,我们称之为字面量类型。每一个字面量类型都只有一个可能的值,即字面量本身。
boolean字面量类型
boolean字面量类型只有以下两种:
- true字面量类型
- false字面量类型
原始类型boolean等同于由true字面量类型和false字面量类型构成的联合类型
true字面量类型只能接受true值;同理,false字面量类型只能接受false值
boolean字面量类型是boolean类型的子类型,因此可以将boolean字面量类型赋值给boolean类型。
const a: true = true;
const b: false = false;
let c: boolean;
c = a;
c = b;
string字面量类型
字符串字面量和模板字面量都能够创建字符串。字符串字面量和不带参数的模板字面量可以作为string字面量类型使用。
string字面量类型是string类型的子类型,因此可以将string字面量类型赋值给string类型。
const a: 'hello' = 'hello';
const b: `world` = `world`;
let c: string;
c = a;
c = b;
数字字面量类型
数字字面量类型包含以下两类:
- number字面量类型
- bigint字面量类型
所有的二进制、八进制、十进制和十六进制数字字面量都可以作为数字字面量类型。
除了正数数值外,负数也可以作为数字字面量类型。
number字面量类型和bigint字面量类型分别是number类型和bigint类型的子类型,因此可以进行赋值操作。
const a0: 0b1 = 1;
const b0: 0o1 = 1;
const c0: 1 = 1;
const d0: 0x1 = 1;
const a1: 0b1n = 1n;
const b1: 0o1n = 1n;
const c1: 1n = 1n;
const d1: 0x1n = 1n;
const a0: -10 = -10;
const b0: 10 = 10;
const a1: -10n = -10n;
const b1: 10n = 10n;
const one: 1 = 1;
const num: number = one;
const oneN: 1n = 1n;
const numN: bigint = oneN;
枚举成员字面量类型
之前介绍了联合枚举成员类型。我们也可以将其称作枚举成员字面量类型,因为联合枚举成员类型使用枚举成员字面量形式表示。
enum Direction {
Up,
Down,
Left,
Right,
}
const up: Direction.Up = Direction.Up;
const down: Direction.Down = Direction.Down;
const left: Direction.Left = Direction.Left;
const right: Direction.Right = Direction.Right;
单元类型
单元类型(Unit Type)也叫作单例类型(Singleton Type),指的是仅包含一个可能值的类型。由于这个特殊的性质,编译器在处理单元类型时甚至不需要关注单元类型表示的具体值。
TypeScript中的单元类型有以下几种:
- undefined类型
- null类型
- unique symbol类型
- void类型
- 字面量类型
- 联合枚举成员类型
顶端类型
顶端类型是一种通用类型,有时也称为通用超类型,因为在类型系统中,所有类型都是顶端类型的子类型,或
者说顶端类型是所有其他类型的父类型。顶端类型涵盖了类型系统中所有可能的值。
TypeScript中有以下两种顶端类型:
- any
- unknown
any
在TypeScript中,所有类型都是any类型的子类型。我们可以将任何类型的值赋值给any类型。
需要注意的是,虽然any类型是所有类型的父类型,但是TypeScript允许将any类型赋值给任何其他类型( never类型例外 )。
在any类型上允许执行任意的操作而不会产生编译错误。例如,我们可以读取any类型的属性或者将any类型当作函数调用,就算any类型的实际值不支持这些操作也不会产生编译错误。
在程序中,我们使用any类型来跳过编译器的类型检查。如果声明了某个值的类型为any类型,那么就相当于告诉编译器:“不要对这个值进行类型检查。”当TypeScript编译器看到any类型的值时,也会对它开启“绿色通道”,让其直接通过类型检查。
在将已有的JavaScript程序迁移到TypeScript程序的过程中,使用any类型来暂时绕过类型检查是一项值得掌握的技巧。
从长远来看,我们应该尽量减少在代码中使用any类型。因为只有开发者精确地描述了类型信息,TypeScript编译器才能够更加准确有效地进行类型检查,这也是我们选择使用TypeScript语言的主要原因之一。
--noImplicitAny
TypeScript中的类型注解是可选的。若一个值没有明确的类型注解,编译器又无法自动推断出它的类型,那么这个值的默认类型为any类型。
function f1(x) {
// ~
// 参数x的类型为any
console.log(x);
}
function f2(x: any) {
console.log(x);
}
此例中,函数f1的参数x没有使用类型注解,编译器也无法从代码中推断出参数x的类型。于是,函数f1的参数x将隐式地获得any类型。最终,函数f1的类型等同于函数f2的类型。在这种情况下,编译器会默默地忽略对参数x的类型检查,这会导致编译器无法检查出代码中可能存在的错误。
在大多数情况下,我们想要避免上述情况的发生。因此,TypeScript提供了一个“--noImplicitAny”编译选项来控制该行为。当启用了该编译选项时,如果发生了隐式的any类型转换,那么会产生编译错误,注意:如果显示注解为 any 类型,不会产生编译错误。
function f(x) {
// ~
// 编译错误!参数'x'具有隐式的'any'类型
console.log(x);
}
unknown
TypeScript 3.0版本引入了另一种顶端类型unknown。unknown类型使用unknown关键字作为标识。
任何其他类型都能够赋值给unknown类型,该行为与any类型是一致的。
unknown类型是比any类型更安全的顶端类型,因为unknown类型只允许赋值给any类型和unknown类型,而不允许赋值给任何其他类型,该行为与any类型是不同的。
同时,在unknown类型上也不允许执行绝大部分操作。
在程序中使用unknown类型时,我们必须将其细化为某种具体类型,否则将产生编译错误。
let x: unknown;
// 错误
x + 1;
x.foo;
x();
function f1(message: any) {
return message.length;
// ~~~~~~
// 无编译错误 但执行可能会报错
}
f1(undefined);
function f2(message: unknown) {
return message.length;
// ~~~~~~
// 编译错误!属性'length'不存在于'unknown'类型上 执行也可能会报错
}
f2(undefined);
我们使用typeof运算符去检查参数message是否为字符串,只有当message是一个字符串时,我们才会去读取其length属性。这样修改之后,既不会产生编译错误,也不会产生运行时错误。
function f2(message: unknown) {
if (typeof message === 'string') {
return message.length;
}
}
f2(undefined);
小结
下面我们将对两者进行简单的对比与总结:
- TypeScript中仅有any和unknown两种顶端类型。
- TypeScript中的所有类型都能够赋值给any类型和unknown类型,相当于两者都没有写入的限制。
- any类型能够赋值给任何其他类型,唯独不包括马上要介绍的never类型。
- unknown类型仅能够赋值给any类型和unknown类型。
- 在使用unknown类型之前,必须将其细化为某种具体类型,而使用any类型时则没有任何限制。
- unknown类型相当于类型安全的any类型。这也是在有了any类型之后,TypeScript又引入unknown类型的根本原因
在程序中,我们应尽量减少顶端类型的使用,因为它们是拥有较弱类型约束的通用类型。如果在编码时确实无法知晓某个值的类型,那么建议优先使用unknown类型来代替any类型,因为它比any类型更加安全。
尾端类型
在类型系统中,尾端类型(Bottom Type)是所有其他类型的子类型。由于一个值不可能同时属于所有类型,例如一个值不可能同时为数字类型和字符串类型,因此尾端类型中不包含任何值。尾端类型也称作0类型或者空类型。
TypeScript中只存在一种尾端类型,即never类型。
never
TypeScript 2.0版本引入了仅有的尾端类型—never类型。never类型使用never关键字来标识,不包含任何可能值。
function f(): never {
throw new Error();
}
根据尾端类型的定义,never类型是所有其他类型的子类型。所以,never类型允许赋值给任何类型,尽管并不存在never类型的值。
let x: never;
let a: boolean = x;
let b: string = x;
let c: number = x;
let d: bigint = x;
let e: symbol = x;
let f: void = x;
let g: undefined = x;
let h: null = x;
正如尾端类型其名,它在类型系统中位于类型结构的最底层,没有类型是never类型的子类型。因此,除never类型自身外,所有其他类型都不能够赋值给never类型。
需要注意的是,就算是类型约束最宽松的any类型也不能够赋值给never类型。
let x: never;
let y: never;
// 正确
x = y;
// 错误
x = true;
x = 'hi';
x = 3.14;
x = 99999n;
x = Symbol();
x = undefined;
x = null;
x = {};
x = [];
x = function () {};
let x: any;
let y: never = x;
// ~
// 编译错误:类型'any'不能赋值给类型'never'
应用场景
never类型主要有以下几种典型的应用场景。
场景一
never类型可以作为函数的返回值类型,它表示该函数无法返回一个值。我们知道,如果函数体中没有使用return语句,那么在正常执行完函数代码后会返回一个undefined值。在这种情况下,函数的返回值类型是void类型而不是never类型。只有在函数根本无法返回一个值的时候,函数的返回值类型才是never类型。
一种情况就是函数中抛出了异常,这会导致函数终止执行,从而不会返回任何值。在这种情况下,函数的返回值类型为never类型。
function throwError(): never {
throw new Error();
// <- 该函数永远无法执行到末尾,返回值类型为'never'
}
若函数中的代码不是直接抛出异常而是间接地抛出异常,那么函数的返回值类型也是never类型。
function throwError(): never {
throw new Error();
}
function fail(): never {
return throwError();
}
除了抛出异常之外,还有一种情况函数也无法正常返回一个值,即如果函数体中存在无限循环从而导致函数的执行永远也不会结束,那么在这种情况下函数的返回值类型也为never类型。
此例中,infiniteLoop函数的执行永远也不会结束,这意味着它无法正常返回一个值。因此,该函数的返回值类型为never类型。
function infiniteLoop(): never {
while (true) {
console.log('endless...');
}
}
场景二
在“条件类型”中常使用never类型来帮助完成一些类型运算。例如,“Exclude<T, U>”类型是TypeScript内置的工具类型之一,它借助于never类型实现了从类型T中过滤掉类型U的功能。
type Exclude<T, U> = T extends U ? never : T;
//我们使用“Exclude<T, U>”工具类型从联合类型“boolean | string”中剔除了string类型,最终得到的结果类型为boolean类型。
type T = Exclude<boolean | string, string>; // boolean
场景三
最后一个要介绍的never类型的应用场景与类型推断功能相关。在TypeScript编译器执行类型推断操作时,如果发现已经没有可用的类型,那么推断结果为never类型。
function getLength(message: string) {
if (typeof message === 'string') {
message; // string
} else {
message; // never
}
}
在if语句中使用typeof运算符来判断message是否为string类型。若参数message为string类型,则执行该分支内的代码。因此,第3行中参数message的类型为string类型。
在else分支中参数message的类型应该是非string类型。由于函数声明中定义了参数message的类型是string类型,因此else分支中已经不存在其他可选类型。在这种情况下,TypeScript编译器会将参数message的类型推断为never类型,表示不存在这样的值。
数组类型
数组是十分常用的数据结构,它表示一组有序元素的集合。在TypeScript中,数组值的数据类型为数组类型。
数组类型定义
TypeScript提供了以下两种方式来定义数组类型:
- 简便数组类型表示法
- 泛型数组类型表示法
简便数组类型表示法
简便数组类型表示法借用了数组字面量的语法,通过在数组元素类型之后添加一对方括号“[]”来定义数组类型
TElement[]
该语法中,TElement代表数组元素的类型,方括号“[]”代表数组类型。在TElement与“[]”之间不允许出现换行符号。
如果数组中元素的类型为复合类型,则需要在数组元素类型上使用分组运算符,即小括号。
const red: (string | number)[] = ['f', f, 0, 0, 0, 0];
泛型数组类型表示法
泛型数组类型表示法是另一种表示数组类型的方法。顾名思义,泛型数组类型表示法就是使用泛型来表示数组类型。
Array<TElement>
该语法中,Array代表数组类型;“<TElement>”是类型参数的语法,其中TElement代表数组元素的类型。
在使用泛型数组类型表示法时,就算数组中元素的类型为复合类型也不需要使用分组运算符。
const red: Array<string | number> = ['f', 'f', 0, 0,0, 0];
两种方法比较
简便数组类型表示法和泛型数组类型表示法在功能上没有任何差别,两者只是在编程风格上有所差别。
数组元素类型
在定义了数组类型之后,当访问数组元素时能够获得正确的元素类型信息。
const digits: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8,9];
const zero = digits[0];
// ~~~~
// number类型
虽然没有给常量zero添加类型注解,但是TypeScript编译器能够从数组类型中推断出zero的类型为number类型。
我们知道,当访问数组中不存在的元素时将返回undefined值。TypeScript的类型系统无法推断出是否存在数组访问越界的情况,因此即使访问了不存在的数组元素,还是会得到声明的数组元素类型。
const digits: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8,9];
// 没有编译错误
const out: number = digits[100];
只读数组
只读数组与常规数组的区别在于,只读数组仅允许程序读取数组元素而不允许修改数组元素。
TypeScript提供了以下三种方式来定义一个只读数组:
- 使用“ReadonlyArray<T>”内置类型。
- 使用readonly修饰符
- 使用“Readonly<T>”工具类型
以上三种定义只读数组的方式只是语法不同,它们在功能上没有任何差别。
ReadonlyArray<T>
在TypeScript早期版本中,提供了“ReadonlyArray<T>”类型专门用于定义只读数组。在该类型中,类型参数T表示数组元素的类型。
const red: ReadonlyArray<number> = [255, 0, 0];
readonly
TypeScript 3.4版本中引入了一种新语法,使用readonly修饰符能够定义只读数组。在定义只读数组时,将readonly修饰符置于数组类型之前即可。
const red: readonly number[] = [255, 0, 0];
readonly修饰符不允许与泛型数组类型表示法一起使用。
const red: readonly Array<number> = [255, 0, 0];
// ~~~~~~~~
// 编译错误
Readonly<T>
“Readonly<T>”是TypeScript提供的一个内置工具类型,用于定义只读对象类型。该工具类型能够将类型参数T的所有属性转换为只读属性
// 工具实现原理
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
const red: Readonly<number[]> = [255, 0, 0];
类型参数T的值为数组类型“number[]”,而不是数组元素类型number。在这一点上,它与“ReadonlyArray<T>”类型是有区别的。
注意事项
我们可以通过数组元素索引来访问只读数组元素,但是不能修改只读数组元素。
在只读数组上也不支持任何能够修改数组元素的方法,如push和pop方法等。
在进行赋值操作时,允许将常规数组类型赋值给只读数组类型,但是不允许将只读数组类型赋值给常规数组类型。换句话说,不能通过赋值操作来放宽对只读数组的约束。
元组类型
元组(Tuple)表示由有限元素构成的有序列表。在JavaScript中,没有提供原生的元组数据类型。TypeScript对此进行了补充,提供了元组数据类型。由于元组与数组之间存在很多共性,因此TypeScript使用数组来表示元组。
在TypeScript中,元组类型是数组类型的子类型。元组是长度固定的数组,并且元组中每个元素都有确定的类型。
元组的定义
定义元组类型的语法与定义数组字面量的语法相似
[T0, T1, ..., Tn]
该语法中的T0、T1和Tn表示元组中元素的类型,针对元组中每一个位置上的元素都需要定义其数据类型。
元组中每个元素的类型不必相同。
元组的值实际上是一个数组,在给元组类型赋值时,数组中每个元素的类型都要与元组类型的定义保持兼容。例如,对于“[number,number]”类型的元组,它只接受包含两个number类型元素的数组。
若数组元素的类型与元组类型的定义不匹配,则会产生编译错误。
let point: [number, number];
point = [0, 'y']; // 编译错误
point = ['x', 0]; // 编译错误
point = ['x', 'y']; // 编译错误
在给元组类型赋值时,还要保证数组中元素的数量与元组类型定义中元素的数量保持一致,否则将产生编译错误。
let point: [number, number];
point = [0]; // 编译错误
point = [0, 0, 0]; // 编译错误
只读元组
元组可以定义为只读元组,这与只读数组是类似的。只读元组类型是只读数组类型的子类型。定义只读元组有以下两种方式:
- 使用readonly修饰符
- 使用“Readonly<T>”工具类型
以上两种定义只读元组的方式只是语法不同,它们在功能上没有任何差别。
readonly
TypeScript 3.4版本中引入了一种新语法,使用readonly修饰符能够定义只读元组。在定义只读元组时,将readonly修饰符置于元组类型之前即可。
const point: readonly [number, number] = [0, 0];
Readonly<T>
由于TypeScript 3.4支持了使用readonly修饰符来定义只读元组,所以从TypeScript 3.4开始可以使用“Readonly<T>”工具类型来定义只读元组。
const point: Readonly<[number, number]> = [0, 0];
此例中,point是包含两个元素的只读元组。在“Readonly<T>”类型中,类型参数T的值为元组类型“[number, number]”。
注意事项
在给只读元组类型赋值时,允许将常规元组类型赋值给只读元组类型,但是不允许将只读元组类型赋值给常规元组类型。换句话说,不能通过赋值操作来放宽对只读元组的约束。
const a: [number] = [0];
const ra: readonly [number] = [0];
const x: readonly [number] = a; // 正确
const y: [number] = ra; // 编译错误
访问元组中的元素
由于元组在本质上是数组,所以我们可以使用访问数组元素的方法去访问元组中的元素。在访问元组中指定位置上的元素时,编译器能够推断出相应的元素类型。
const score: [string, number] = ['math', 100];
const course = score[0]; // string
const grade = score[1]; // number
const foo: boolean = score[0];
// ~~~
// 编译错误!类型 'string' 不能赋值给类型 'boolean'
const bar: boolean = score[1];
// ~~~
// 编译错误!类型 'number' 不能赋值给类型 'boolean'
当访问数组中不存在的元素时不会产生编译错误。与之不同的是,当访问元组中不存在的元素时会产生编译错误。
const score: [string, number] = ['math', 100];
const foo = score[2];
// ~~~~~~~~
// 编译错误!该元组类型只有两个元素,找不到索引为'2'的元素
修改元组元素值的方法与修改数组元素值的方法相同。
元组类型中的可选元素
在定义元组时,可以将某些元素定义为可选元素。定义元组可选元素的语法是在元素类型之后添加一个问号“?”。
如果元组中同时存在可选元素和必选元素,那么可选元素必须位于必选元素之后。
[T0, T1?, ..., Tn?]
该语法中的T0表示必选元素的类型,T1和Tn表示可选元素的类型
const tuple: [boolean, string?, number?] = [true,'yes', 1];
在给元组赋值时,可以不给元组的可选元素赋值。
let tuple: [boolean, string?, number?] = [true, 'yes',1];
tuple = [true];
tuple = [true, 'yes'];
tuple = [true, 'yes', 1];
元组类型中的剩余元素
在定义元组类型时,可以将最后一个元素定义为剩余元素。
[...T[]]
该语法中,元组的剩余元素是数组类型,T表示剩余元素的类型。
const tuple: [number, ...string[]] = [0, 'a', 'b'];
如果元组类型的定义中含有剩余元素,那么该元组的元素数量是开放的,它可以包含零个或多个指定类型的剩余元素。
let tuple: [number, ...string[]];
tuple = [0];
tuple = [0, 'a'];
tuple = [0, 'a', 'b'];
tuple = [0, 'a', 'b', 'c'];
元组的长度
对于经典的元组类型,即不包含可选元素和剩余元素的元组而言,元组中元素的数量是固定的。也就是说,元组拥有一个固定的长度。TypeScript编译器能够识别出元组的长度并充分利用该信息来进行类型检查。
function f(point: [number, number]) {
// 编译器推断出length的类型为数字字面量类型2
const length = point.length;
if (length === 3) { // 编译错误!条件表达式永远为 false
// ...
}
}
此例第3行,TypeScript编译器能够推断出常量length的类型为数字字面量类型2。第5行在if条件表达式中,数字字面量类型2与数字字面量类型3没有交集。因此,编译器能够分析出该比较结果永远为false。在这种情况下,编译器将产生编译错误。
当元组中包含了可选元素时,元组的长度不再是一个固定值。编译器能够根据元组可选元素的数量识别出元组所有可能的长度,进而构造出一个由数字字面量类型构成的联合类型来表示元组的长度。
const tuple: [boolean, string?, number?] = [true,'yes', 1];
let len = tuple.length; // 1 | 2 | 3
len = 1;
len = 2;
len = 3;
len = 4; // 编译错误!类型'4'不能赋值给类型'1 | 2 | 3'
若元组类型中定义了剩余元素,那么该元组拥有不定数量的元素。因此,该元组length属性的类型将放宽为number类型。
const tuple: [number, ...string[]] = [0, 'a'];
const len = tuple.length; // number
元组类型与数组类型的兼容性
元组类型是数组类型的子类型,只读元组类型是只读数组类型的子类型。在进行赋值操作时,允许将元组类型赋值给类型兼容的元组类型和数组类型。
const point: [number, number] = [0, 0];
const nums: number[] = point; // 正确
const strs: string[] = point; // 编译错误
元组类型允许赋值给常规数组类型和只读数组类型,但只读元组类型只允许赋值给只读数组类型。
const t: [number, number] = [0, 0];
const rt: readonly [number, number] = [0, 0];
let a: number[] = t;
let ra: readonly number[];
ra = t;
ra = rt;
由于数组类型是元组类型的父类型,因此不允许将数组类型赋值给元组类型。
const nums: number[] = [0, 0];
let point: [number, number] = nums;
// ~~~~~
// 编译错误