TypeScript 中的非空类型

Marius Schulz, “Non-Nullable Types in TypeScript”, September 28, 2016

TypeScript 2.0 引入了很多新特性。本篇,我将介绍 非空类型,这是对类型系统的基础功能提升,帮助我们避免了编译期间的空值错误(nullability errors)。

nullundefined

TypeScript 2.0 之前,类型检查器(type checker)允许将 nullundefined 赋值给 任意 类型。这包括字符串、数值和布尔值在内的原始类型:

let name: string;
name = "Marius";  // OK
name = null;      // OK
name = undefined; // OK

let age: number;
age = 24;        // OK
age = null;      // OK
age = undefined; // OK

let isMarried: boolean;
isMarried = true;      // OK
isMarried = false;     // OK
isMarried = null;      // OK
isMarried = undefined; // OK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

number 类型为例。它所表示的范围不仅包括 IEEE 754 浮点数值,还包括 nullundefined

Domains of TypeScript's number type

对象、数组和函数类型也是如此。但在这种类型系统下,无法表示非空的变量。幸运的是,TypeScript 2.0 修复了这个问题。

严格空值检查

TypeScritp 2.0 添加了对 非空类型(non-nullable types) 的支持。命令行中使用 --strictNullChecks flag 就能启用 严格空值检查(strict null checking) 模式,或者在项目的 tsconfig.json 文件中增加 strictNullChecks 编译选项:

{
  "compilerOptions": {
    "strictNullChecks": true
    // ...
  }
}
1
2
3
4
5
6

严格空值检查模式下,nullundefined 不再能够赋值给任意类型。nullundefined 都归属在各自类型之下:

Domains of TypeScript's number, null, and undefined types

启用严格空值检查模式后,将 nullundefined 赋值给任何变量都会导致错误:

// 使用 --strictNullChecks flag 编译的结果
    
let name: string;
name = "Marius";  // OK
name = null;      // Error
name = undefined; // Error

let age: number;
age = 24;        // OK
age = null;      // Error
age = undefined; // Error

let isMarried: boolean;
isMarried = true;      // OK
isMarried = false;     // OK
isMarried = null;      // Error
isMarried = undefined; // Error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

那么,我们如何在 TypeScript 2.0 中表示可为空变量呢?

使用联合类型构建可为空变量

由于启用严格空值检查后,各个类型默认都是非空的,所以,我们需要明确告诉类型检查器一个变量是可为空的。这可以通过将 nullundefined 混入联合类型做到:

let name: string | null;
name = "Marius";  // OK
name = null;      // OK
name = undefined; // Error
1
2
3
4

注意,undefined 并不是可以赋值给变量 name 的有效值,因为联合类型中并没有包含 undefined

这种表示可为空变量的方式非常直观。我们以一个简单的 User 类型为例:

type User = {
  firstName: string;
  lastName: string | undefined;
};

let jane: User = { firstName: "Jane", lastName: undefined };
let john: User = { firstName: "John", lastName: "Doe" };
1
2
3
4
5
6
7

可以通过在 lastName 后面加 ? 的方式将属性标记为可选的。而且,undefined 会自动添加到联合类型。因此,下面的赋值都是类型正确(type-correct)的:

type User = {
  firstName: string;
  lastName?: string;
};

// We can assign a string to the "lastName" property
let john: User = { firstName: "John", lastName: "Doe" };

// ... or we can explicitly assign the value undefined
let jane: User = { firstName: "Jane", lastName: undefined };

// ... or we can not define the property at all
let jake: User = { firstName: "Jake" };
1
2
3
4
5
6
7
8
9
10
11
12
13

访问可为空属性

如果一个对象变量是可为空的,直接访问它的任意属性都会导致编译期错误(compil-time error):

function getLength(s: string | null) {
  // Error: Object is possibly 'null'.
  return s.length;
}
1
2
3
4

访问属性前,需要使用 type guard 保证访问对象属性是安全的:

function getLength(s: string | null) {
  if (s === null) {
    return 0;
  }

  return s.length;
}
1
2
3
4
5
6
7

TypeScript 理解 JavaScript 的真值语义(truthiness semantics),支持条件表达式中的 type guard。因此,下面这种检查方式也是可以的:

function getLength(s: string | null) {
  return s ? s.length : 0;
}
1
2
3

调用可为空函数

如果一个函数是可为空的,直接调用它就会导致编译期错误。以下面的 callback 参数为例:

function doSomething(callback?: () => void) {
  // Error: Object is possibly 'undefined'.
  callback();
}
1
2
3
4

类似于访问对象属性前的检查,在调用函数前也要先检查,以便确保不是空值:

function doSomething(callback?: () => void) {
  if (callback) {
    callback();
  }
}
1
2
3
4
5

如果喜欢,还能通过 typeof 操作符确保返回值不是空值:

function doSomething(callback?: () => void) {
  if (typeof callback === "function") {
    callback();
  }
}
1
2
3
4
5

总结

非空类型是 TypeScript 类型系统中非常基础和有价值的一个功能。它允许我们明确指定变量或属性是不是可为空,确保属性访问和函数调用是安全的,避免了编译期间的空值错误。

本篇文章是 TypeScript Evolution 系列中的一篇。