TypeScript$Variable-Narrowing
在声明变量的时候,变量的类型就确定了。有可能变量的类型是多个(比如函数参数),这样在使用的时候,我们只能使用多个类型的交集的功能。为了能够使用具体的类型的功能,我们需要区分到底是什么类型,也就是把多个类型缩小到几个或一个类型,这个过程就是 narrowing。
在 JavaScript 中,我们也会区分类型(通过类型或者值),比如通过 typeof, ===, in,instanceof 等。这些方法在 TypeScript 中也一样。
除了 JavaScript 中区分类型的方式,TypeScript 也有独特的方式。
1. 一下就能想到的方式
1.1 === & Boolean() 通过变量的值
有时候我们可以通过变量的值来 narrow。=== 、 !== 以及 Boolean()是常用的方式。在 if() 中的条件也可以当做使用了 Boolean()。
// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true, value: true
1.2 typeof 通过变量的类型
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") { // type regard
return " ".repeat(padding) + input;
}
return padding + input;
}
1.3 instanceof 通过对象的类
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}
1.4 in 通过对象的属性
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
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
}
}
2. TypeScript 特有的方式
2.1 Using type predicates 使用类型谓词
To define a user-defined type guard, we simply need to define a function whose return type is a type predicate:
有时候为了更加直接的判断类型,我们可以定义一个函数来判断变量的类型。这个函数的返回值的类型是 type predicate。type predicates 的形式是 parameterName is Type。如果函数返回 true,则表示该变量是这个类型(parameterName is Type)。
这个函数体内的逻辑就是用来判断 parameterName 是否是类型 Type。换句话说,之前我们是通过 typeof 等方式来判断变量的类型,现在把这部分逻辑放在了函数中。(好处是以后可以直接通过该函数判断类型,代价是写函数时需要使用 type predicate 这种结构)
官方的一个例子如下。按照上面的描述,如何判断一个变量 pet 的类型是不是 Fish 呢?(前提是 pet 是 Fish 或者 Bird)只要 pet 会 swim 就行了(这里假设 Bird 不会游泳)。
这里面需要理解的就两点:
pet is Fish这种形式- 函数体内根据
Fish的特点返回true/false(这里也利用了参数Fish | Bird) 是不是特别清晰呢?😎
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or, equivalently
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];
// The predicate may need repeating for more complex examples
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
if (pet.name === "sharkey") return false;
return isFish(pet);
});
this-based type guards
2.2 Assertion functions
2.3 Discriminated unions 判别联合
Discriminated unions 主要用来设计“类族”。比如 Shape 类有 Circle 和 Square 等“子类”。为了区分这些子类,可以增加一个唯一标识 kind。这样当处理 Shape 时,就可以通过 switch 来判断每种类型,从而处理每种情况。😏
When every type in a union contains a common property with literal types, TypeScript considers that to be a discriminated union, and can narrow out the members of the union.
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}
In this case, kind was that common property (which is what’s considered a discriminant property of Shape).
The never type
When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never type to represent a state which shouldn’t exist.
2.4 Exhaustiveness checking 穷举检查
Exhaustiveness checking 利用了 discriminated unions 和 never 类型,可以防止程序扩展时出错。方式是在 switch 中把所有的 discriminant property 都列出来, 并在 default 里返回 never。这样当扩展 discriminated unions 时,如果没有对 switch 做调整,就会报错:
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
// error. Type 'Triangle' is not assignable to type 'never'.
return _exhaustiveCheck;
}
}