前言

文中局部内容(typeof 、in、instanceof、类型谓词等)曾经在TypeScript根底之类型爱护介绍过。
文中内容都是官网https://www.typescriptlang.org/docs/handbook/2/narrowing.html 内容,以及参考 TypeScript 之 Narrowing---mqyqingfeng

类型收窄(Type Narrowing)

当初有一个名为padLeft的函数, 须要实现的性能是: 如果参数 padding 是一个数字,咱们就在 input 后面增加等同数量的空格,而如果 padding 是一个字符串,咱们就间接增加到 input 后面。
实现:

function padLeft(padding: number | string, input: string) {  return "".repeat(padding) + input;  // 类型“string | number”的参数不能赋给类型“number”的参数。  // 不能将类型“string”调配给类型“number”。}

如果这样写的话,编辑器里padding会提醒类型“string | number”的参数不能赋给类型“number”的参数谬误。 提醒咱们应该先查看下padding是否是一个number
所以下一步批改为:

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

以上代码中 TypeScript 在背地做了很多货色。
TypeScript 要学着剖析这些应用了动态类型的值在运行时的具体类型。目前 TypeScript 曾经实现了比方 if/else 、三元运算符、循环、真值查看等状况下的类型剖析。

if 语句中,TypeScript 会认为 typeof padding === number 是一种非凡模式的代码,咱们称之为类型爱护 (type guard),TypeScript 会沿着执行时可能的门路,剖析值在给定的地位上最具体的类型。

TypeScript 的类型查看器会思考到这些类型爱护和赋值语句,而这个将类型推导为更准确类型的过程,咱们称之为收窄 (narrowing)。 在编辑器中,咱们能够察看到类型的扭转:

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

从上图中能够看到在 if 语句中,和残余的 return 语句中,padding 的类型都推导为更准确的类型。

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

typeof 类型爱护(type guards)

对于typeof在之前文章里曾经介绍过了,这里再介绍下。
在 TypeScript 中,查看typeof返回的值就是一种类型爱护。

function printAll(strs: string | string[] | null) {  if (typeof strs === "object") {    for (const s of strs) {      // strs 提醒 对象可能为 "null"。      console.log(s);    }  } else if (typeof strs === "string") {    console.log(strs);  } else {    // do nothing  }}

在这个 printAll 函数中,咱们尝试判断 strs 是否是一个对象,本来的目标是判断它是否是一个数组类型,然而在 JavaScript 中,typeof null 也会返回 object。
TypeScript 会让咱们晓得 strs 被收窄为 strings[] | null ,而不仅仅是 string[]

真值收窄(Truthiness narrowing)

在 JavaScript 中,咱们能够在条件语句中应用任何表达式,比方 && 、||、! 等,举个例子,像 if 语句就不须要条件的后果总是 boolean 类型:

function getUsersOnlineMessage(numUsersOnline: number) {  if (numUsersOnline) {    return `There are ${numUsersOnline} online now!`;  }  return "Nobody's here. :(";}

这时因为 JavaScript 会做隐式类型转换, 像空字符串、0、-0 、0n、NaN、null、undefined 和 false这些值都会被转为false, 其余则会被转为true。
你也能够应用Boolean函数强制转换为 boolean 值, 或者应用!!

Boolean("hello");  // true!!"world";      // true

真值收窄这种应用形式十分风行,尤其实用于防备 null和 undefiend 这种值的时候。
如:

function printAll(strs: string | string[] | null) {  if (strs && typeof strs === "object") {    for (const s of strs) {      console.log(s);    }  } else if (typeof strs === "string") {    console.log(strs);  }}

能够看到通过这种形式,胜利的去除了谬误。
但还是要留神,在根本类型上的真值查看很容易导致谬误,比方:

function printAll(strs: string | string[] | null) {  // !!!!!!!!!!!!!!!!  //  DON'T DO THIS!  //   KEEP READING  // !!!!!!!!!!!!!!!!  if (strs) {    if (typeof strs === "object") {      for (const s of strs) {        console.log(s);      }    } else if (typeof strs === "string") {      console.log(strs);    }  }}

if (strs) 真值查看里,存在一个问题,就是咱们无奈正确处理空字符串的状况。如果传入的是空字符串,真值查看判断为 false,就会进入谬误的解决分支。

等值收窄(Equality narrowing)

TypeScript中也会应用switch语句和等值查看比方===、 !==、 == 、 !=去收窄类型。 如:

function example(x: string | number, y: string | boolean) {  if (x === y) {    // We can now call any 'string' method on 'x' or 'y'.    x.toUpperCase();    // (method) String.toUpperCase(): string        y.toLowerCase();    // (method) String.toLowerCase(): string  } else {    console.log(x);    // (parameter) x: string | number    console.log(y);    // (parameter) y: string | boolean  }}

以上代码中,咱们判断了 x 和 y 是否齐全相等,如果齐全相等,那他们的类型必定也齐全相等。而 string 类型就是 x 和 y 惟一可能的雷同类型。所以在第一个分支里,x 和 y 就肯定是 string 类型。
判断具体的字面量值也能让 TypeScript 正确的判断类型。
咱们晓得: undefined == null 为true, 利用这点能够不便的判断一个值既不是null也不是undefined

interface Container {  value: number | null | undefined;}function multiplyValue(container: Container, factor: number) {  //  排除调null 、undefined  if (container.value != null) {    console.log(container.value);                               // (property) Container.value: number     container.value *= factor;  }}

in 操作符收窄

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

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  }}

以上代码里, Human里有swim、fly两个可选属性, TS通过in 操作符能够筹备的进行类型收窄。

instanceof 收窄

instanceof 也是一种类型爱护,TypeScript 也能够通过辨认 instanceof 正确的类型收窄

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

赋值语句(Assignments)

TypeScript 能够依据赋值语句的右值,正确的收窄左值。

let x = Math.random() < 0.5 ? 10 : "hello world!"; // let x: string | numberx = 1;console.log(x);        // let x: numberx = "goodbye!"; console.log(x);        // let x: string

以上赋值语句都有无效的,即使咱们曾经将 x 改为 number 类型,但咱们仍然能够将其更改为 string 类型,这是因为 x 最后的申明为 string | number,赋值的时候只会依据正式的申明进行核查。
所以如果咱们把 x 赋值给一个 boolean 类型,就会报错

x = false;// 不能将类型“false”调配给类型“string | number”

控制流剖析(Control flow analysis)

来看看在 if while等条件管制语句中的类型爱护, 如:

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

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

这种基于可达性(reachability) 的代码剖析就叫做控制流剖析(control flow analysis)。在遇到类型爱护和赋值语句的时候,TypeScript 就是应用这样的形式收窄类型。

应用类型谓词(Using type predicates)

所谓 predicate 就是一个返回 boolean 值的函数。
如果你想间接通过代码管制类型的扭转, 你能够自定义一个类型爱护。实现形式是定义一个函数,这个函数返回的类型是类型判断式,如:

function isFish(pet: Fish | Bird): pet is Fish {  return (pet as Fish).swim !== undefined;}

在这个例子中,pet is Fish就是咱们的类型判断式,一个类型判断式采纳 parameterName is Type的模式,但 parameterName 必须是以后函数的参数名。
当 isFish 被传入变量进行调用,TypeScript 就能够将这个变量收窄到更具体的类型:

class Fish {  swim () {    console.log('游泳~');  }  eat () {    console.log('进食!');  }}class Bird {  fly () {    console.log('翱翔~');  }  eat () {    console.log('进食!');  }}function getSmallPet(): Fish | Bird {  return Math.random() > 0.5 ? new Fish() : new Bird()}let pet = getSmallPet();function isFish(pet: Fish | Bird): pet is Fish {  return (pet as Fish).swim !== undefined;}if (isFish(pet)) {    pet.swim();     // let pet: Fish} else {    pet.fly();      // let pet: Bird}

可分别联结(Discriminated unions)

interface Circle {  kind: "circle";  // 字符串字面量类型  radius: number;} interface Square {  kind: "square";   // 字符串字面量类型  sideLength: number;}type Shape = Circle | Square;function getArea(shape: Shape) {  return Math.PI * shape.radius ** 2;     // 类型“Shape”上不存在属性“radius”。  // 类型“Square”上不存在属性“radius”}

报错,是因为 Shape 是一个联结类型,TypeScript 能够辨认出 shape 也可能是一个 Square,而 Square 并没有 radius,所以会报错。
通过判断字面量类型来进行辨别:

function getArea (shape: Shape) {  switch (shape.kind) {    case "circle":  // Circle类型      return Math.PI * shape.radius ** 2;    case "square":  // Square类型      return shape.sideLength ** 2;  }}

当联结类型中的每个类型,都蕴含了一个独特的字面量类型的属性,TypeScript 就会认为这是一个可分别联结(discriminated union),而后能够将具体成员的类型进行收窄。
在这个例子中,kind 就是这个公共的属性(作为 Shape 的可分别(discriminant) 属性 )。

never 类型

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

穷尽查看(Exhaustiveness checking)

never 类型能够赋值给任何类型,然而,没有类型能够赋值给 never (除了 never 本身)。这就意味着你能够在 switch 语句中应用 never 来做一个穷尽查看 .
接下面例子, 新增加一个新成员,却没有做对应解决的时候,就会导致一个 TypeScript 谬误:

interface Triangle {  kind: "triangle";  sideLength: number;}type Shape = Circle | Square | Triangle;function getArea(shape: Shape) {  switch (shape.kind) {    case "circle": // Circle类型      return Math.PI * shape.radius ** 2;    case "square": // Circle类型      return shape.sideLength ** 2;    default:      const _exhaustiveCheck: never = shape; // 不能将类型“Triangle”调配给类型“never”      return _exhaustiveCheck;  }}

因为 TypeScript 的收窄个性,执行到 default 的时候,类型被收窄为 Triangle,但因为任何类型都不能赋值给 never 类型,这就会产生一个编译谬误。通过这种形式,你就能够确保 getArea 函数总是穷尽了所有 shape 的可能性。

参考资料

https://www.typescriptlang.org/docs/handbook/2/narrowing.html
TypeScript 之 Narrowing---mqyqingfeng