前言
文中局部内容(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
,如果 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