前言
文中局部内容(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
,如果 padding
是 number
类型,是无奈达到 (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