TS 中何时使用“never”与“unknown”类型
Wed, Jul 24, 19
Philadelphia's Magic Gardens. This place was so cool!
图:Philadelphia's Magic Gardens. This place was so cool!

TypeScript 在版本 2.0 和 3.0 分别引入了 “never” 和 “unknown” 两个基本类型。这完善了 TS 类型系统的基础性和全面性。TypeScript 严格遵循了类型设计原则;同时,它也是一门实用主义语言,它引入的每一个特性都有其实际用途,这包括 neverunknown。欲准确理解这些特性的用法,我们首先要问“究竟什么是类型?”。

以集合理论作解

当你深入去思考一下何为“类型”,你会发现,所谓之“类型”,乃不过为可能取得值之集合。举个例子,Typescript 中, 类型 string 乃全部字符串的集合。类型 Date 乃全部 Date 实例的集合,而类型 Iterable<T> 乃全部实现了接口 Iterable 并对迭代项目约束以指定类型 T 的对象的集合。

Typescript 对基本类型的设计执念于集合理论,此外,它还有并集(union)和 交集(intersection)等高级类型。类型 string | number 就是一个 “union” 类型,因为它表达的是全部字符串的集合与全部数值集合的合并。

Philadelphia's Magic Gardens. This place was so cool!
图:Philadelphia's Magic Gardens. This place was so cool!

因为 string | number 包含了全部的 string 和 全部的 number,故它是类型 stringnumber 的超级类型(supertype)。

unknown 是某些值的集合,任何值都能冠以类型 unknown。这意味着 unknown 是一切类型的超级类型(supertype)。这就是为什么 unknown 被称为顶端类型。

Philadelphia's Magic Gardens. This place was so cool!
图:Philadelphia's Magic Gardens. This place was so cool!

集合(或曰类型,可视作同义词)unknown 包含了一切其它集合。

never 是一个空集合,任何值都不能冠以类型 never。实际上,如果你将某个值的类型解析为 never,系统将提出抗议,因为这么做存在矛盾之处。空集合可包含于任何非空集合,因此 never 是一切其它非空类型的子集合。这就是为什么 never 被称为底端集合。

底端和顶端集合可分别借助操作符 union|) 和 intersection&)来识别,比如,给定类型 T,则:

T | never => T
T & unknown => T

这可类比于,任何数加上 0 并不改变这个数,任何数乘以 1 亦如此。0 是可用加法来识别,而 1 则可以用乘法来识别。

任何集合与空集合作并运算,并不对该集合有所改变,因此 never 可以 unions 运算识别;而交集运算是取两个集合的相同部分,但是 unknown 包含了一切,因此 unknown 可以 intersection 运算识别。

因为只有类型 never 能够在类型 union 运算中得到识别,下面我们将看到它在一些情况下的不可或缺性。

never 用于那些永不可发生的情况

我们来写一段代码,它用于发出一个网络请求,但是因为花费时间过久而失败。我们可以使用 Promise.race 来将这个持有网络请求返回值的 promise 和另一个在给定时间之内就会被 rejectpromise 合并起来。以下为第二个 promise 的构造函数:

function timeout(ms: number): Promise<never> {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error("Timeout!")), ms)
  })
}

注意返回值 promise 的解析值类型,因为这个 promise 决不会调用 resolve,我们可以使用 any 作为其类型,这并无冲突。但是我们既然可以具体到类型 never,何不用之?

现在来看看对超时的操作:

async function fetchPriceWithTimeout(tickerSymbol: string): Promise<number> {
  const stock = await Promise.race([
    fetchStock(tickerSymbol),
    timeout(3000)
  ])
  return stock.price
}

很完美!但是编译器如何推断 Promise.race 的返回值类型呢?race 取最先被 settled 的那个 promise,在这个例子中,Promise.race 的签名应该像这样:

function race<A, B>(inputs: [Promise<A>, Promise<B>]): Promise<A | B>

返回的 promise 所解析值的类型是两个 promise 解析值类型的合集,上述例子中,fetchStocktimeout 为输入,因此它们的解析值的类型 A{ price: number },而 Bnever,因此,函数输出的 promise 解析值类型为 { price: number } | never。 因为 neverunions 运算的识别因子,故返回值可简化为 { price: number },这正是我们希望的。

如果我们不使用 never 作为 timeout 的返回值类型,那么,我们不可能表达得如此干净利索。如果我们使用 any,那么我们将失去类型检查的好处,因为 { price: number } | anyany 等同。

如果我们使用 unknown,那么 stock 的类型将会是 { price: number } | unknown,也就是 unknown。如此,我们就不能访问到属性 price,因为,price 属性信息已经丢失。

用 never 来收敛条件类型 (conditional type)

你会经常看到,never 被用于条件类型,以排除掉不需要的情况。举个例子吧,下面这些条件类型从函数的类型定义中抽离出参数和返回值的类型:

type Arguments<T> = T extends (...args: infer A) => any ? A : never
type Return<T> = T extends (...args: any[]) => infer R ? R : never

function time<F extends Function>(fn: F, ...args: Arguments<F>): Reurn<F> {
  console.time()
  const result = fn(...args)
  console.timeEnd()
  return result
}

T 是函数类型,则编译器推断其参数类型和返回值类型。但如果 T 不是函数类型,那么 ArgumentReturn 返回者便无以感知。这里,我们使用的是 never 来约束,如此,我们可得编译时 error:

// Error: Type '3' is not assignable to type 'never'
const x: Return<"not a function type"> = 3

对于收敛(narrowing)联合类型(union type),条件类型堪为可用。在 TS 库中,NonNullable 类型即是例证,它将 nullundefined 类型从 T 中排除。其定义大概如此:

type NonNullable<T> = T extends null | undefined ? never : T

条件类型能对联合类型作分布处理,此所以上述代码可行。类似形为 T extends U ? X : Y 类型者,T 代表一未知集合,施以条件,则可对组成 T 的子集以此二元运算,然后将结果合并,以得结果。

// if T = A | B
T extends U ? X : T == (A extends U ? X : A) | (B extends U ? X : B)

可以看到,在每一个分支里,子集都按照二元运算或替换以新的类型、或维持原类型。

因而,类型 NonNullable<string | null> 可按照如下几步解析:

type NullOrUndefined = null | undefined
type NonNullable<string | null> 
  // The conditional distributes over each branch in `string | null`
  == (string extends NullOrUndefined ? never : string) | (null extends  NullOrUndefined ? never : null)
  // The conditional in each union branch is resolved
  == string | never
  // `never` factors out of the resulting union type
  == string

结果为,对于给定的集合(类型) TNonNullable 生成一个更小的集合,这里使用 never 将无需的类型排除掉。

使用 unknown 代表万物

任何值都能冠以 unknown 类型,因此,任何集合包含于 unknown 中。在不便更明确地指定类型时,可使用之。举个例子,pretty-printing 函数能接收一切类型的值:

function prettyPrint(x: unknown): string {
  if (Array.isArray(x)) {
    return "["+ x.map(prettyPrint).join(", ") +"]"
  }
  if (typeof x === "string") {
    return `"${x}"`
  }
  if (typeof x === "number") {
    return String(x)
  }
  return "etc."
}

直接使用 unknown 没什么意义,但是你可借助“类型守卫”在块级作用域内收敛类型,并由此获得准确的类型检查。

TS 3.0 之前,定义 prettyPrint 函数参数 x 类型为 any 极为可取。类型收敛如 unknown 一样,可用于 any。在将 x 的类型收敛于 Array 类型的块级域中,类型检查允许我们访问到 xmapjoin 方法。我们使用 unknown 类型好处之一就是类型检查会对任何成员访问施以错误提醒,而 any 无此提醒。

import isArray from  'isarray'

function prettyPrint(x: any): string {
  if (isArray(x)) { // isArray 非类型守卫
    return "[" + x.map(prettyPrint).join(", ")
  }
  return "ect."
}

isarray 无类型定义,故而无法使函数 isArray 为类型守卫。但我们很可能使用了 isarray 而未能发现问题,为什么呢?因为,isArray 非类型守卫,而我们将 x 限制于类型 any,因而在 if 块级域内,x 仍为 any。这导致 TS 编译器在此无法捕获到 x 的类型,而如果我们将 x 限制以 unknown 情况将有所不同:

Object is of type “unknown”

使用 unknown 更加安全!

如何在 never、unknown、any 之间作出选择

prettyPrint 函数的参数类型与上述函数 timeout 所返回 promise 的解析值的类型都可冠以 any。不同之处是,timeoutpromise 的解析值不是任意的,因为它根本不可能解析出任何值。

  • 在那些将或既不能取得任何值的地方,使用 never
  • 在那些将或既取得任意值,但不知类型的地方,请使用 unknown
  • 除非你有意忽略类型检查,不要使用 any

总之,你应该尽量使用具体的类型。never 是最具体的类型,因为没有哪个集合比空集合更小了;而 unknown 是最弱的类型,因为它包含了全部可能的值。any 则不为集合,它破坏了类型检查,因此请尽量不要使用 any


https://blog.logrocket.com/when-to-use-nev...
TypescriptTranslation