红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

TypeScript 学习笔记 2:类型缩小

2026年2月11日 5点热度 0人点赞 0条评论

在前篇文章中,提到过在使用联合类型时可能需要进行类型缩小,比如:

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.

参考资料

  • TypeScript 官方手册

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: ts
最后更新:2026年2月11日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号