TypeScript$Variable-Narrowing

86 阅读4分钟

TypeScript$Variable-Narrowing

在声明变量的时候,变量的类型就确定了。有可能变量的类型是多个(比如函数参数),这样在使用的时候,我们只能使用多个类型的交集的功能。为了能够使用具体的类型的功能,我们需要区分到底是什么类型,也就是把多个类型缩小到几个或一个类型,这个过程就是 narrowing。

在 JavaScript 中,我们也会区分类型(通过类型或者值),比如通过 typeof, ===, ininstanceof 等。这些方法在 TypeScript 中也一样。

除了 JavaScript 中区分类型的方式,TypeScript 也有独特的方式。

1. 一下就能想到的方式

1.1 === & Boolean() 通过变量的值

有时候我们可以通过变量的值来 narrow。===!== 以及 Boolean()是常用的方式。在 if() 中的条件也可以当做使用了 Boolean()

// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true,    value: true

1.2 typeof 通过变量的类型

function padLeft(padding: number | string, input: string): string {
  if (typeof padding === "number") { // type regard
    return " ".repeat(padding) + input;
  }
  return padding + input;
}

1.3 instanceof 通过对象的类

function logValue(x: Date | string) {
  if (x instanceof Date) {
    console.log(x.toUTCString());
  } else {
    console.log(x.toUpperCase());
  }
}

1.4 in 通过对象的属性

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

2. TypeScript 特有的方式

2.1 Using type predicates 使用类型谓词

To define a user-defined type guard, we simply need to define a function whose return type is a type predicate:

有时候为了更加直接的判断类型,我们可以定义一个函数来判断变量的类型。这个函数的返回值的类型是 type predicatetype predicates 的形式是 parameterName is Type。如果函数返回 true,则表示该变量是这个类型(parameterName is Type)。

这个函数体内的逻辑就是用来判断 parameterName 是否是类型 Type。换句话说,之前我们是通过 typeof 等方式来判断变量的类型,现在把这部分逻辑放在了函数中。(好处是以后可以直接通过该函数判断类型,代价是写函数时需要使用 type predicate 这种结构)

官方的一个例子如下。按照上面的描述,如何判断一个变量 pet 的类型是不是 Fish 呢?(前提是 pet 是 Fish 或者 Bird)只要 pet 会 swim 就行了(这里假设 Bird 不会游泳)。

这里面需要理解的就两点:

  1. pet is Fish 这种形式
  2. 函数体内根据 Fish 的特点返回 true / false(这里也利用了参数 Fish | Bird) 是不是特别清晰呢?😎
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
 
if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or, equivalently
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];
 
// The predicate may need repeating for more complex examples
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
  if (pet.name === "sharkey") return false;
  return isFish(pet);
});

this-based type guards

this is Type

2.2 Assertion functions

Assertion functions

2.3 Discriminated unions 判别联合

Discriminated unions 主要用来设计“类族”。比如 Shape 类有 Circle 和 Square 等“子类”。为了区分这些子类,可以增加一个唯一标识 kind。这样当处理 Shape 时,就可以通过 switch 来判断每种类型,从而处理每种情况。😏

When every type in a union contains a common property with literal types, TypeScript considers that to be a discriminated union, and can narrow out the members of the union.

interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  sideLength: number;
}
 
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;
  }
}

In this case, kind was that common property (which is what’s considered a discriminant property of Shape).

The never type

When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never type to represent a state which shouldn’t exist.

2.4 Exhaustiveness checking 穷举检查

Exhaustiveness checking 利用了 discriminated unions 和 never 类型,可以防止程序扩展时出错。方式是在 switch 中把所有的 discriminant property 都列出来, 并在 default 里返回 never。这样当扩展 discriminated unions 时,如果没有对 switch 做调整,就会报错:

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;
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}
interface Triangle {
  kind: "triangle";
  sideLength: number;
}
 
type Shape = Circle | Square | Triangle;
 
function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape;
// error. Type 'Triangle' is not assignable to type 'never'.
      return _exhaustiveCheck;
  }
}

Links

TypeScriptNarrowing