类型守卫

我们可以把类型守卫的逻辑判断当作是一条流过的河流。

declare const strOrNumOrBool: string | number | boolean;

if (typeof strOrNumOrBool === "string") {
  // 一定是字符串!
  strOrNumOrBool.charAt(1);
}

但是如果我们把 typeof 的类型判断单独抽离出来,会发现情况有所不同。

function isString(input: unknown): input is string {
  return typeof input === "string";
}

function foo(input: string | number) {
  if (isString(input)) {
    // 类型 string | number 上不存在属性 replace
    (input).replace("aaa", "bbb")
  }
  if (typeof input === 'number') { }
  // ...
}

想象类型控制流分析这条河流,刚流进 if (isString(input)) 就戛然而止了。因为 isString 这个函数在另外一个地方,内部的判断逻辑并不在函数 foo 中。这里的类型控制流分析做不到跨函数上下文来进行类型的信息收集

但是将判断逻辑封装起来提取到函数外部进行复用非常常见。为了解决这一类型控制流分析的能力不足, TypeScript 引入了 is 关键字来显式地提供类型信息。

function isString(input: unknown): input is string {
  return typeof input === "string";
}

function foo(input: string | number) {
  if (isString(input)) {
    // 正确
    (input).replace("aaa", "bbb")
  }
  if (typeof input === 'number') { }
  // ...
}

isString 函数称为类型守卫,在它的返回值中,我们不再使用 boolean 作为类型标注,而是使用 input is string 的搭配。

input is string 拆开分析含义是:

  • input,函数的某个参数
  • is string,即 is 关键字 + 预期类型,即如果这个函数成功返回为 true,那么 is 关键字前这个入参的类型,就会被这个类型守卫调用方后续的类型控制流分析收集到。

从这个角度来看,其实类型守卫有些类似于类型断言,但类型守卫更宽容,也更信任你一些。你指定什么类型,它就是什么类型。

下面是两个开发常用的类型守卫。

export type Falsy = false | "" | 0 | null | undefined;

export const isFalsy = (val: unknown): val is Falsy => !val;

// 不包括不常用的 symbol 和 bigint
export type Primitive = string | number | boolean | undefined

export const isPrimitive = (val: unknown): val is Primitive => ['string', 'number', 'boolean' , 'undefined'].includes(typeof val);