原始类型

JavaScript 语言中的每种原始类型都有与之对应的 TypeScript 类型。除此之外,TypeScript 还对原始类型进行了细化与扩展,增加了枚举类型和字面量类型等。

到目前为止,TypeScript 中的原始类型包含以下几种:

  • boolean

  • string

  • number

  • bigint

  • symbol

  • undefined

  • null

  • void

  • 枚举类型

  • 字面量类型

本节将详细介绍除枚举类型和字面量类型之外的原始类型。对于枚举类型和字面量类型,我们将在独立的章节中介绍它们。

boolean

TypeScript 中的 boolean 类型对应于 JavaScript 中的 Boolean 原始类型。该类型能够表示两个逻辑值:truefalse

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 值视作表示固定值的字面量,TypeScriptunique 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 值的字面量形式。

如果使用 letvar 声明定义 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');

此例中,编译器会认为 ab 是两个不同的 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 类型指的是值可以为 undefinednull 的类型。

JavaScript 中有两个比较特殊的原始类型,即 Undefined 类型和 Null 类型。两者分别仅包含一个原始值,即 undefined 值和 null 值,它们通常用来表示某个值还未进行初始化。

TypeScript 早期的版本中,没有提供与 JavaScriptUndefined 类型和 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

null 类型只包含一个可能值,即 null 值。null 类型使用 null 关键字标识。示例如下:

const foo: null = null;

null 和 undefined 区别

  • 使用 undefined 表示未初始化的状态。

  • 使用 null 表示明确的“空”值。

特性 undefined null

含义

未定义、未初始化

明确为空

类型默认值

声明但未赋值的变量默认为 undefined

null 需要手动赋值

常见用途

用于检查变量是否未初始化或未定义

用于明确指明变量的值为空

null 和 undefined 的相等性

  • 宽松相等 (==): nullundefined 是相等的(但不完全相等)。

  • 完全相等 (===): 它们是不同的类型,因此严格相等时会返回 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 类型的变量值可以为 undefinednull,那么这段代码在运行时将产生错误。示例如下:

/**
 * --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;
}