原始类型
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 位二进制浮点数格式存储的数字。
number
类型使用 number
关键字来表示。示例如下:
// 二进制数
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.4 节中介绍过,字面量能够表示一个固定值。例如,数字字面量 “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
类型的设计初衷是作为一种变通方法,让一个 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
值,其类型为 unique symbol
类型。在标识符 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
类型。示例如下:
const a: unique symbol = Symbol();
const b: symbol = a;
如果程序中未使用类型注解来明确定义是 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;
关于类型推断的详细介绍请参考7.3节。
Nullable
TypeScript 中的 Nullable
类型指的是值可以为 undefined
或 null
的类型。
JavaScript 中有两个比较特殊的原始类型,即 Undefined
类型和 Null
类型。两者分别仅包含一个原始值,即 undefined
值和 null
值,它们通常用来表示某个值还未进行初始化。
在 TypeScript 早期的版本中,没有提供与 JavaScript 中 Undefined
类型和 Null
类型相对应的类型。TypeScript 允许将 undefined
值和 null
值赋值给任何其他类型。虽然在 TypeScript 语言的内部实现中确实存在这两种原始类型,但是之前没有将它们开放给开发者使用。
TypeScript 2.0 版本的一个改变就是增加了 undefined
类型和 null
类型供开发者使用。虽然看上去是一项普通的改进,但却有着非凡的意义。因为,不当地使用 undefined
值和 null
值是程序缺陷的主要来源之一,并有可能导致价值亿万美元的错误。相信一定有不少读者都曾经遇到过如下的 JavaScript 程序错误:
TypeError: Cannot read property 'xxx' of undefined
现在,在 TypeScript 程序中能够明确地指定某个值的类型是否为 undefined
类型或 null
类型。TypeScript 编译器也能够对代码进行更加细致的检查以找出程序中潜在的错误。
undefined
undefined
类型只包含一个可能值,即 undefined
值。undefined
类型使用 undefined
关键字标识。示例如下:
const foo: undefined = undefined;
null 和 undefined 区别
-
使用
undefined
表示未初始化的状态。 -
使用
null
表示明确的“空”值。
特性 | undefined | null |
---|---|---|
含义 |
未定义、未初始化 |
明确为空 |
类型默认值 |
声明但未赋值的变量默认为 undefined |
null 需要手动赋值 |
常见用途 |
用于检查变量是否未初始化或未定义 |
用于明确指明变量的值为空 |
null 和 undefined 的相等性
-
宽松相等 (
==
):null
和undefined
是相等的(但不完全相等)。 -
完全相等 (
===
): 它们是不同的类型,因此严格相等时会返回false
。
console.log(null == undefined); // true
console.log(null === undefined); // false
--strictNullChecks
TypeScript 2.0 还增加了一个新的编译选项 --strictNullChecks
,即严格的 null
检查模式。虽然该编译选项的名字中只提及了 null
,但实际上它同时作用于 undefined
类型和 null
类型的类型检查。
在默认情况下,--strictNullChecks
编译选项没有被启用。这时候,除尾端类型外的所有类型都是 Nullable
类型。也就是说,除尾端类型外所有类型都能够接受 undefined
值和 null
值。关于尾端类型的详细介绍请参考 5.8 节。
例如,在没有启用 --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
类型。
还是以上例中的代码为例,如果我们启用了 --strictNullChecks
编译选项,那么 TypeScript 编译器就能够检查出代码中的错误。示例如下:
/**
* --strictNullChecks=true
*/
let foo: string = undefined;
// ~~~
// 编译错误!类型 'undefined' 不能赋值给类型 'string'
foo.length;
此例第 4 行,TypeScript 在执行静态类型检查时就能够发现这处类型错误,从而避免了在代码运行时才发现这个缺陷。
前面我们说在启用了 --strictNullChecks
编译选项时,undefined
值只能够赋值给 undefined
类型,null
值只能够赋值给 null
类型,实际上这种表述不完全准确。因为在该模式下,undefined
值和 null
值允许赋值给 顶端类型,同时 undefined
值也允许赋值给 void
类型。这些类型在后面的章节中会有详细介绍。示例如下:
/**
* --strictNullChecks=true
*/
let m1: void = undefined;
let m2: any = undefined;
let m3: unknown = undefined;
let n2: any = null;
let n3: unknown = null;
undefined
类型和 null
类型是不同的类型,它们必须被区分对待,不能互换使用。示例如下:
/**
* --strictNullChecks=true
*/
const foo: undefined = null;
// ~~~
// 编译错误!类型 'null' 不能赋值给类型 'undefined'
const bar: null = undefined;
// ~~~
// 编译错误!类型 'undefined' 不能赋值给类型 'null'
在了解了 --strictNullChecks
编译选项的作用后,让我们来看一看如何启用该编译选项。在默认情况下,--strictNullChecks
编译选项没有被启用,我们需要在工程下的 tsconfig.json
配置文件中启用该编译选项,通过将 strictNullChecks
属性设置为 true
就能够启用 --strictNullChecks
编译选项。同理,如果将该属性设置为 false
则会关闭该编译选项。关于配置文件的详细介绍请参考 8.3 节。示例如下:
{
"compilerOptions": {
"strictNullChecks": true
}
}
如果读者使用的是 TypeScript 官网提供的在线代码编辑器,则可以在 Config
菜单中找到该选项并选中,即可启用该选项。
void
void
类型表示某个值不存在,该类型用作函数的返回值类型。若一个函数没有返回值,那么该函数的返回值类型为 void
类型。除了将 void
类型作为函数返回值类型外,在其他地方使用 void
类型是无意义的。关于函数类型的详细介绍请参考 5.12 节。
void
类型使用 void
关键字来表示。示例如下:
function log(message: string): void {
console.log(message);
}
此例中,log
函数的参数类型为 string
,返回值类型为 void
,表示该函数 “没有” 返回值。
当启用了 --strictNullChecks
编译选项时,只允许将 undefined
值赋值给 void
类型。示例如下:
/**
* --strictNullChecks=true
*/
// 正确
function foo(): void {
return undefined;
}
// 编译错误!类型 'null' 不能赋值给类型 'void'
function bar(): void {
return null;
}
如果没有启用 --strictNullChecks
编译选项,那么允许将 undefined
值和 null
值赋值给 void
类型。示例如下:
/**
* --strictNullChecks=false
*/
// 正确
function foo(): void {
return undefined;
}
// 正确
function bar(): void {
return null;
}