作者 | geekAbyte

以下为译文:

在本文中,我们来简要地讨论一下 TypeScript 中的三种有趣的类型:any、unknown 与 never。我们来快速了解一下这三种类型,以及何时使用它们。

首先,集合论非常适合用来分析类型系统。TypeScript 中的 Union 和 Interp 类型就大量使用了集合论。但其思想非常简单。定义一个类型类似于定义一个集合。集合包含什么?它们包含对象。所以,对象属于一个集合。如果某个值属于某个集合,则不能在它不属于或不重叠的另一个集合中使用。

例如,类型 string 是所有字符串的集合,这个集合的数量是无限的;类型 boolean 是所有布尔值的集合,但布尔值是有限的,只有 true 和 false。基本思想就这么简单。

下面,我们来探索一下 Typescript 中的 any、unknown 与 never。

any 

any 是一种类型,它包含 Javascript 中的所有值。因此,如果你有一个值,可能来自某个没有类型注释的 Javascript 库,或者定义类型很麻烦,那么只需将其类型设置为 any 即可通过类型检查。这是因为如果把 any 当成一个集合,那么它是一个超集,包含所有值。

从理论的角度来看,any 类型被称为顶层类型(或称通用类型、通用超类型),因为所有类型的值都可以赋给它,这意味着它就是所有类型的通用类型,如下列代码所示:

let value: any;


let boolValue: boolean = true
let numValue: number = 43
let strValue: string = "Hello world"


value = boolValue
value = numValue
value = strValue

在上述代码中,boolean、number 和 string 类型的值都可以分配给 any。Javascript 中的任何类型,甚至是自定义的类型都可以分配给 any。

然而,如果代码编写成上面这样,我们就会失去类型安全。一旦将一个值赋给 any,typescript 的编译器就无法确定原始的类型,也不清楚哪些操作是允许的,而哪些是禁止的。结果,你就不会再看到红色的波浪线或编译错误,但是原本应该在开发过程中发现的错误,就会一直留到运行时才能暴露出来。

例如:

let value: any;


let boolValue: boolean = true


value = boolValue
value.charCodeAt(0)

在上述代码中,先是将 any 赋给了 value,然后又把一个 boolean 值赋给了它。在这之后,却用 value 调用了一个只有字符串值才能使用的方法。这是错误的,但是编译器无法指出这一点。所以,这种写法不可取。

那么,我们应该在什么时候使用 any 呢?也许在创建原型,希望尽快实现一个可行方案的时候可以使用它。你不应该在生产中使用 any,因为如果使用 any,那么一切都将失去控制。在代码库中乱用 any,就与直接使用 Javascript 没有区别了,所以即便使用了 Typescript,也不会获得类型安全的好处,最终只会让 any 注释搅得代码乱七八糟。

既然我们不能使用 any,那么有其他的替代类型吗?当然有,请参见接下来的讨论。

unknown

unknown 也是 Typescript 中的顶层类型。这意味着,所有类型的值都可以赋给 unknown 类型。

let value: unknown;


let boolValue: boolean = true
let numValue: number = 43
let strValue: string = "Hello world"


value = boolValue
value = numValue
value = strValue

unknown 与 any 最大的区别在于,使用 unknown 的时候,编译器不允许你调用任何方法。任何方法调用都会引发编译错误,如下所示:

let value: unknown;


let boolValue: boolean = true


value = boolValue
// compile error
value.charCodeAt(0)

这段代码就会引发编译错误。如果编译器无法确定哪些操作有效,那么就不能允许任何操作,否则就可能会产生无效操作,导致运行时出错?

其实,我们需要考虑的问题是,何时使用 unknown,以及如何使用?

只有在针对 unknown 进行类型检查后,Typescript 编译器才会允许你执行特定的操作。例如:

let value: unknown;


let boolValue: boolean = true


value = boolValue


if (typeof value === "string") {
  value.charCodeAt(0)
}

这意味着,如果你想使用 unknown 类型的值,则必须在执行操作之前进行类型检查。这个过程被称为类型窄化(Type Narrowing)。

那么,我们应该在何时使用 unknown 呢?通常在需要使用 any,但同时又希望编译器提供类型安全的时候,就可以使用 unknown,因为编译器会强制你在调用某个方法之前,手动检查该类型是否允许这样的操作。

never

如果说 any 是顶层类型(Top type),那么 never 就是底层类型(Bottom type)这是什么意思?如果说顶层类型就是包含所有类型的值的类型,那么底层类型就是不包含任何类型的值的类型。实际上,底层类型是一个空集。

如果类型不包含任何东西,那么我们要它干什么呢?

我们可以利用 never 表示一种情况:某个永远无法返回值的操作。例如,一个永远无法返回值的函数。如下代码是一个连续输出时间戳的函数:

function neverReturns(): never {
  while(true) {
    console.log(Date.now())
  }
}

这个函数是什么类型?它不会返回任何值。因此,就应该将 never 类型赋给它。

在处理联合类型时,也可以使用 never。在这种情况下,never 表示不存在这种值,从而确保类型安全,如下列代码所示。

假设有一个联合类型 number | boolean 的值,我们可以使用类型窄化来编写代码,以确保该值确实为 number 或 boolean:

let value: number | boolean = 1 


function process(value: number | boolean) {
  if (typeof value === "number") {
    // TODO operate on value as number
  } else {
    // TODO operate on value as boolean
  }
}

这种写法的问题在于,如果将来扩展 value 的联合类型,再增加一种类型,则 process 无法处理这个新类型。因此,如果将一个 boolean 值传递给函数,则在运行时会出错,而且编译器也无法发出警告。为了解决这个问题,我们可以使用 never 类型。具体的方法是再加入一个判断分支,接收输入值,并将它分配给另一个类型为 never 的值。意思是永远不会出现这种情况,如果真的出现了,则意味着判断分支中存在未处理的值。将这样的值赋给 never 类型,就会发生编译错误。我们可以通过 never 的这种使用方法来获得更多类型安全。这个过程叫做穷举类型检查,这种机制可以让我们在编译时考虑到联合类型中所有可能出现的类型。

never 类型的使用不仅限于此,还有其他很有趣的高级使用方法。但是,一般来说,never 类型的意思就是永远不包含任何值。