在前篇文章中,提到过在使用联合类型时可能需要进行类型缩小,比如:
function concatStr(prefix: string | number, context: string) {
if (typeof prefix === "number") {
// 如果前缀是数字,返回若干个空格和字符串拼接的结果
return " ".repeat(prefix) + context;
}
// 如果前缀是字符串,直接拼接后返回
return prefix + context;
}
console.log(concatStr(2,"hello"))
console.log(concatStr("hello","world"))
类型守卫
在上面的例子中,我们使用运算符typeof缩小类型,这被称为类型守卫(type guards)。
typeof运算符返回的值包括:
-
string
-
number
-
bigint
-
boolean
-
symbol
-
undefined
-
object
-
function
注意,这个检查结果中并不包括null,看下面这个示例:
function printAll(strs:string|string[]|null){
if(typeof strs === "object"){
for(const str of strs){ // 这里会报错——“strs”可能为“null”
console.log(str)
}
}
else if(typeof strs === "string"){
console.log(strs)
}
else{
// 你可能期望这里是 null
}
}
这段代码中值得注意的是:
-
在 JavaScript 中,数组是对象。
-
null 也是对象。
因此这里需要仔细考虑如何处理“变量是否为空值”的问题,这会在下一节讨论。
真值缩小
在其他编程语言中,在条件表达式中往往只能使用 boolean 类型的两个值:true 或者 false。
但在 JavaScript 中你会看到奇怪的代码:
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
console.log(getUsersOnlineMessage(11)) // There are 11 online now!
console.log(getUsersOnlineMessage(0)) // Nobody's here. :(
这是因为 JavaScript 会将一些表示空值的内容转换为false,具体包括:
-
0
-
NaN
-
""(空字符串)
-
0n(bigint 类型的 0)
-
null
-
undefined
对应的,其他的值会被转换为true。
可以通过两种方式显式地将表达式的值转换为 boolean:
Boolean("hello") // TS 推断类型是 boolean
!!"hello" // TS 推断结果是 true
两者的区别在于使用操作符!!的方式,TS 可以在可能的情况下推断出具体的 boolean 值作为类型。
在理解上面的内容后,我们可以解决之前的 null 问题:
function printAll2(strs:string|string[]|null){
if(strs && typeof strs === "object"){
for(const str of strs){
console.log(str)
}
}
else if(typeof strs === "string"){
console.log(strs)
}
}
printAll2("hello")
printAll2(["how","are","you"])
现在不会因为遍历 null 导致的异常了。但需要注意的是,检查空值时需要小心:
function printAll3(strs: string | string[] | null) {
if (strs) {
if (typeof strs === "object") {
for (const str of strs) {
console.log(str)
}
}
else if (typeof strs === "string") {
console.log(strs)
}
}
}
printAll3("hello")
printAll3(["how", "are", "you"])
printAll3(null)
printAll3("")
if(strs)不仅会排除strs是null的情况,同样会排除strs是空字符串的情况。
相等缩小
使用相等判断同样可以缩小类型范围,比如:
function example(var1: string | number, var2: string | boolean){
if (var1 === var2) {
// 这里 var1 和 var2 的类型必然都为 string
console.log(var1.toUpperCase());
console.log(var2.toLowerCase());
}
else {
console.log(var1);
console.log(var2);
}
}
example("hello", "world")
example(1, true)
example("hello", "hello")
同样的,对于之前的问题,我们可以使用显式判断去排除,而非粗略的if(strs):
function printAll5(strs: string | string[] | null) {
if (strs !== null) {
if (typeof strs === "object") {
for (const str of strs) {
console.log(str)
}
}
else if (typeof strs === "string") {
console.log(strs)
}
}
}
当然,这里也可以使用!=而非绝对不相等!==,但前者很容易出错。
in 缩小
JavaScript 存在一个操作符in,可以用于检查对象是否有特定的属性,这同样可以被用于缩小类型:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
// 这里类型必然是 Fish
return animal.swim();
}
// 这里类型必然是 Bird
return animal.fly();
}
move({swim: () => {console.log("swim")}})
move({fly: () => {console.log("fly")}})
需要注意的是,有可能存在一种类型满足多个in判断,比如:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim: () => void; fly: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
// 这里类型必然是 Fish 或 Human
return animal.swim();
}
else{
// 这里类型必然是 Bird 或 Human
return animal.fly();
}
}
instanceof 缩小
在 JavaScript 中,可以使用操作符instanceof判断变量是否使用某个类型的原型(prototype)创建,比如:
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
}
else {
console.log(x.toUpperCase());
}
}
logValue(new Date())
logValue("hello")
显然,这里实现了类型缩小,TS 可以很容易推断出对应的类型。
赋值
对变量赋值时,TS 会缩小变量的类型,比如:
let x = Math.random() < 0.5 ? 10 : "hello world!";
// 缩小后,x 的类型为 number | string
x = 1
console.log(x)
// 缩小后,x 的类型为 number
x = "goodbye!"
console.log(x)
// 缩小后,x 的类型为 string
需要注意的是,TS 会在重新赋值时对新值是否符合初始类型进行检查,比如:
let x = Math.random() < 0.5 ? 10 : "hello world!";
// 缩小后,x 的类型为 number | string
x = 1
console.log(x)
// 缩小后,x 的类型为 number
x = "goodbye!"
console.log(x)
// 缩小后,x 的类型为 string
x = true
// 报错,因为初始类型是 number | string,而 true 不属于 number | string
控制流
当分析一个变量时,控制流可以多次分叉并重新合并,并且该变量在不同点上可能具有不同的类型:
function example2() {
let x: string | number | boolean;
x = Math.random() < 0.5;
console.log(x);
// x 的类型被缩小为 boolean
if (Math.random() < 0.5) {
x = "hello";
console.log(x);
// x 的类型被缩小为 string
} else {
x = 100;
console.log(x);
// x 的类型被缩小为 number
}
return x;
// x 的类型被缩小为 string | number
}
使用类型谓词
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function getSmallPet(): Fish | Bird {
if (Math.random() < 0.5) {
return { swim: () => { console.log("swim") } };
}
return { fly: () => { console.log("fly") } };
}
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
这里pet is Fish是类型谓词,其中is前的部分必须是方法参数中的一个。isFishi方法就是一个自定义的类型守卫。可以利用这个类型守卫筛选数组:
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1 = zoo.filter(isFish);
// 类型缩小为 Fish[]
也可以显式描述缩小后的类型:
const underWater2 = zoo.filter(isFish) as Fish[];
也可以使用匿名函数定义自定义类型守卫:
const underWater3 = zoo.filter((pet): pet is Fish => {
return (pet as Fish).swim !== undefined;
});
区分联合
看一个示例:
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
// 报错,“shape.radius”可能为“undefined”
}
return shape.sideLength ** 2;
// 报错,“shape.sideLength”可能为“undefined”
}
Shape类型可以是圆形或正方形,通过kind属性进行区分,当是圆形时具有radius属性,如果是正方形,具有sideLength属性。
但是这种潜在的关联关系 TS 并不知晓,因此getArea中通过shape.kind筛选后不能访问相应的属性,TS 会认为可能不存在。
可以使用!操作符表示我很确定这里的确存在对应的属性:
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius! ** 2;
}
return shape.sideLength! ** 2;
}
现在 TS 的编译器不会报错了,但这样很不优雅。
可以使用联合类型:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape2 = Circle | Square;
function getArea2(shape: Shape2) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
return shape.sideLength ** 2;
}
这里将圆形和正方形拆分成了两个类型进行定义,它们拥有共同的属性kind,但又拥有各自独特的属性。自定义类型Shape2是这两个类型的联合,getArea2方法接收Shape2类型的参数,并且可以通过kind属性实现类型缩小,可以很容易地判断shape.kind === "circle"筛选后的类型是Circle。
这里最关键的点是
kind属性都是字面量类型,因此 TS 可以根据具体的kind值判断其所属对象的类型。
除了使用if...else...缩小范围,这种情况下还可以使用switch:
function getArea3(shape: Shape2){
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}
console.log(getArea3({ kind: "circle", radius: 5 }))
console.log(getArea3({ kind: "square", sideLength: 10 }))
never 类型
never类型用于表示一个不应该存在的类型。never可以赋值给任何类型,但其他类型不能赋予never类型,能赋值给never的只有 never 自己。
我们可以利用这个特点实现类型检查,比如:
function getArea4(shape: Shape2) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
在上面的例子中,default永远不会触发,所以default中的shape的类型被推断为never,因此这里将shape赋予never类型的_exhaustiveCheck是合法的。
如果扩展类型:
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape3 = Circle | Square | Triangle;
function getArea5(shape: Shape3) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
// 这里会报错,不能将类型“Triangle”分配给类型“never”。
return _exhaustiveCheck;
}
}
在这里const _exhaustiveCheck: never = shape;成功发现了存在类型没有被处理的情况。
本文的完整代码可以从
The End.

文章评论