前言
文中部分内容(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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。