200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > TypeScript基础之类型收窄(Type Narrowing)

TypeScript基础之类型收窄(Type Narrowing)

时间:2019-04-22 07:01:20

相关推荐

TypeScript基础之类型收窄(Type Narrowing)

前言

文中部分内容(typeof 、in、instanceof、类型谓词等)已经在TypeScript基础之类型保护介绍过。

文中内容都是官网/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(): stringy.toLowerCase();// (method) String.toLowerCase(): string} else {console.log(x);// (parameter) x: string | numberconsole.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 、undefinedif (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 的可能性。

参考资料

/docs/handbook/2/narrowing.html

TypeScript 之 Narrowing—mqyqingfeng

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。