关于typescript:TypeScript基础之类型收窄Type-Narrowing

36次阅读

共计 6912 个字符,预计需要花费 18 分钟才能阅读完成。

前言

文中局部内容(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 | number

x = 1;
console.log(x);        
// let x: number

x = "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

正文完
 0