likes
comments
collection
share

TS文档 --- 类型收窄和类型保护

作者站长头像
站长
· 阅读数 22

本篇整理自 TypeScript Handbook 中 「Narrowing」 章节。

Narrowing

// 将类型推导为更精确类型的过程,我们称之为收窄 (narrowing)
function padLeft(padding: number | string, input: string) {
  // typeof padding === number --- 类型保护 (type guard)
  if (typeof padding === "number") {
    return new Array(padding + 1).join(" ") + input;
  }
  return padding + input;
}

在编辑器中,我们可以观察到类型的改变:

TS文档 --- 类型收窄和类型保护

从上图中可以看到在 if 语句中,和剩余的 return 语句中,padding 的类型都推导为更精确的类型。

接下来,我们就介绍 narrowing 所涉及的各种内容。

typeof 类型保护(type guards)

在 TypeScript 中,检查 typeof 返回的值就是一种类型保护。TypeScript 知道 typeof 不同值的结果,它也能识别 JavaScript 中一些怪异的地方

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return new Array(padding + 1).join(" ") + input;
  }
  return padding + input;
}

真值收窄(Truthiness narrowing)

if 语句不需要条件的结果总是 boolean 类型,这是因为 JavaScript 会做隐式类型转换,

0NaN""0nnull undefined 这些值都会被转为 false,其他的值则会被转为 true

也可以使用 Boolean 函数强制转为 boolean 值,或者使用更加简短的!!

function multiplyAll(
  values: number[] | undefined,
  factor: number
): number[] | undefined {
  if (!values) {
    return values;
    // (parameter) values: undefined
  } else {
    return values.map((x) => x * factor);
    // (parameter) values: number[]
  }
}

等值收窄(Equality narrowing)

Typescript 也会使用 switch 语句和等值检查比如 == !== == != 去收窄类型

TS文档 --- 类型收窄和类型保护

判断具体的字面量值也能让 TypeScript 正确的判断类型。

TS文档 --- 类型收窄和类型保护

此时上述的例子中,就可以正常处理strs的值为空字符串的情况

JavaScript 的宽松相等操作符如 ==!= 也可以正确的收窄

不过在 JavaScript 中,通过 == null 这种方式并不能准确的判断出这个值就是 null,它也有可能是 undefined 。对 == undefined 也是一样

TS文档 --- 类型收窄和类型保护

in 操作符收窄

JavaScript 中有一个 in 操作符可以判断一个对象是否有对应的属性名。TypeScript 也可以通过这个收窄类型。

type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
    // (parameter) animal: Fish
  }
 
  return animal.fly();
  // (parameter) animal: Bird
}

instanceof 收窄

instanceof 也是一种类型保护,TypeScript 也可以通过识别 instanceof 正确的类型收窄

TS文档 --- 类型收窄和类型保护

赋值语句(Assignments)

TypeScript 可以根据赋值语句的右值,正确的收窄左值。

TS文档 --- 类型收窄和类型保护

注意这些赋值语句都有有效的,即便我们已经将 x 改为 number 类型,但我们依然可以将其更改为 string 类型,这是因为 x 最初的声明为 string | number赋值的时候只会根据正式的声明进行核对

控制流分析(Control flow analysis)

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return new Array(padding + 1).join(" ") + input;
  }
  return padding + input;
}

在第一个 if 语句里,因为有 return 语句,TypeScript 就能通过代码分析,判断出在剩余的部分 return padding + input ,如果 padding 是 number 类型,是无法达到 (unreachable) 这里的,所以在剩余的部分,就会将 number类型从 number | string 类型中删除掉。

这种基于可达性(reachability) 的代码分析就叫做控制流分析(control flow analysis)。

TS文档 --- 类型收窄和类型保护

类型谓词(type predicates)

所谓 predicate 就是一个返回 boolean 值的函数。

type Fish = { swim: () => void };
type Bird = { fly: () => void };

// pet is Fish 就是类型谓词
// 格式为parameterName is Type
// 其中parameterName 必须是当前函数的参数名
function isFish(pet: Fish | Bird): pet is Fish {
  return 'swim' in pet;
}

可辨别联合(Discriminated unions)

当联合类型中的每个类型,都包含了一个共同的字面量类型的属性,TypeScript 就会认为这是一个可辨别联合(discriminated union),然后可以将具体成员的类型进行收窄。

interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  sideLength: number;
}
 
type Shape = Circle | Square;

在这个例子中,kind 就是这个公共的属性(作为 Shape 的可辨别(discriminant) 属性 )

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
  } else {
    return shape.sideLength ** 2
  }
}

TS文档 --- 类型收窄和类型保护

never 类型

当进行收窄的时候,如果你把所有可能的类型都穷尽了,TypeScript 会使用一个 never 类型来表示一个不可能存在的状态。

穷尽检查(Exhaustiveness checking)

除了 never 自身,没有类型可以赋值给 never。这就意味着你可以在 switch 语句中使用 never 来做一个穷尽检查。

type Shape = Circle | Square;
 
function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
      
    // 只有当Shape的所有类型都被处理完毕
    // default分支中的shape的类型才会是never
    // 当我们给 Shape 类型添加一个新成员,却没有做对应处理的时候,就会导致一个 TypeScript 错误	
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}