TypeScript 中的 Non-Nullable类型

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 的方式能够确保类型安全,从而避免编译时许多空值引起的错误。