Typescript 中的 Non-Nullable类型

2019-03-09

Typescirpt 2.0发布了许多新的特性。在这篇文章中,我们一起学习non-nulable类型。它是对类型系统的重要改进,旨在避免一整类在编译时因为可能为空值引发的报错。

nullundefined

在Typescript 2.0之前,nullundefined可以是任何类型的值,也就是说,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

我们以number类型为例。它的范围不仅包含了所有IEEE 754 floating point numbers,并且也包含了nullundefined这两个特殊值。

对于对象、数组和函数类型也是类似的。所以在之前的类型系统中没有一种方式可以表达一个具体的变量是non-nullable的,即不能为空的。幸运的是,Typescript 2.0解决了这个问题。

严格的Null检测

Typescript 2.0增加了对non-nullable类型的支持。你可以选择通过在命令行加--strictNullChecks标识开启严格null检测模式。或者,你也可以在项目的tsconfig.json配置文件中,添加开启编译选项strictNullChecks

{
  "compilerOptions": {
    "strictNullChecks": true
    // ...
  }
}

在严格null检测模式下,nullundefined都不能再赋值给其他类型。它们只能赋值给各自的nullundefined类型:

我们如果在严格null检测模式下编译上面例子中的代码,会因为将nullundefined赋值给其他类型的变量而报类型错误:

// Compiled with --strictNullChecks

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

那在Typescript 2.0中我们该怎么定义一个nullabe,即可以为空的变量呢?

使用联合类型定义可能为空的变量

在严格null检测下,既然类型默认是non-nullable,即不能为空的,那么我们需要显式地告诉类型检查器哪些变量是可能为空的。我们可以通过构造包含nullundefined的联合类型来解决:

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

注意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" };

我们通过在lastName后面添加?使得这个属性是可选的,所以我们可以完全忽略lastName属性的赋值。另外,undefined类型会自动添加到这种联合类型中。因此,下面的所有赋值都是类型正确的:

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" };

访问可能为空类型的属性

如果一个对象的类型包含了null或者undefined,访问它的任意属性都会导致编译时报错:

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

在访问属性之前,你需要使用type guard(类型收窄)来检查访问这个对象的属性是否安全:

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

  return s.length;
}

Typescript能够理解Javascript的语法,所以在三目运算符中也能支持type guard,下面的代码依然能够正确运行:

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

可能为空值的函数的调用

如果你尝试调用一个类型包含了null或者undefined的函数,就会触发一个编译时错误。下面的callback参数是可选的(注意?),所以它可能是undefined。因此不能被直接调用:

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

和在访问属性之前检查对象一样,我们应该先检查这个函数是否是non-null的,即非空的:

function doSomething(callback?: () => void) {
  if (callback) {
    callback();
  }
}

或者我们也可以通过typeof操作符去检查:

function doSomething(callback?: () => void) {
  if (typeof callback === "function") {
    callback();
  }
}

总结

Non-nullable类型是对Typescript基础类型系统很重要的补充。它们使我们可以精确地控制那些变量和属性是可能为空的。可能为空的对象的属性访问或者函数调用通过type guard的方式能够确保类型安全,从而避免编译时许多空值引起的错误。

郑超的独立博客