📝 引言

📌 场景/痛点

你在写代码时,一定见过这个红色的波浪线:Object is possibly 'undefined'This condition will always return false

很多新手的第一反应是:“烦死了,给它加个 as (或者 !) 吧!”

const data = getData();
// ❌ 暴力强转,危险!
console.log((data as User).name); 

这种做法是饮鸩止渴。 你虽然骗过了编译器,但如果 data 真的是 undefined,你的程序在运行时会直接崩溃。真正的 TypeScript 高手,懂得如何通过类型守卫,让编译器智能地推导出类型的变化。

✨ 最终效果

掌握 Type Guards 后,你的代码逻辑将更加严谨。当你在 if 块中判断了变量不为空后,编译器会自动收窄类型,该变量的属性提示会立刻出现,无需任何强转。

📖 内容概览

本文将带你深入 TS 的类型推导逻辑:

  1. 什么是控制流分析 (Control Flow Analysis)?
  2. 掌握四大内置类型守卫:typeofinstanceofin==/!=
  3. 进阶:自定义类型守卫与 is 关键字。

🛠️ 正文

1. 环境准备

继续在 src/ 下新建 guards.ts。我们将重点演示逻辑块中的类型变化。

2. 理解控制流分析

TypeScript 拥有极强的代码分析能力。它会像人类读代码一样,分析你的 ifelsewhile 等代码块,从而预测变量在特定位置的类型。这个过程叫 Narrowing(类型收窄)。

3. 四大内置类型守卫

3.1 typeof:处理原始类型

最基础也是最常用的守卫。注意,typeof 在 JS 中只返回 7-8 种字符串(如 “string”, “number”, “object”)。

function printId(id: string | number) {
    // 这里 id 是 string | number,不能直接 toUpperCase 或 toFixed
    if (typeof id === "string") {
        // ✅ TS 知道这里 id 一定是 string
        console.log(id.toUpperCase());
    } else {
        // ✅ TS 知道这里 id 一定是 number
        console.log(id.toFixed(2));
    }
}
3.2 instanceof:处理类与实例

当你需要区分数组、日期对象或自定义类的实例时,使用 instanceof

function logValue(value: Date | string[]) {
    if (value instanceof Date) {
        // ✅ 这里被推导为 Date
        console.log(value.getFullYear());
    } else {
        // ✅ 这里被推导为 string[]
        console.log(value.length);
    }
}
3.3 in 操作符:检查对象属性

这是判断对象形状的神器。当你处理“联合类型对象”时,in 非常好用。

interface Fish { swim: () => void; }
interface Bird { fly: () => void; }

function move(animal: Fish | Bird) {
    if ("swim" in animal) {
        // ✅ 只有 Fish 有 swim 属性,这里推导为 Fish
        animal.swim();
    } else {
        // ✅ 这里推导为 Bird
        animal.fly();
    }
}
3.4 等值检查:处理 nullundefined

这是最经典的用法。利用 != null (同时排除 nullundefined) 来收窄类型。

function getUsers() {
    const users: User[] | null | undefined = fetchUsers(); // 假设可能为空
    
    // ❌ 错误:users 可能为 null
    // users.map(u => u.name);

    // ✅ 正确:进行判断
    if (users != null) {
        // ✅ 这里 users 一定是 User[]
        users.map(u => u.name);
        
    }
}

4. 进阶核心:自定义类型守卫 (is 关键字)

有时候,内置的守卫不够用。比如你通过正则判断一个字符串是否是邮箱格式,TS 并不知道这就意味着它是 Email 类型。

这时,我们需要自定义守卫函数。它的返回值类型不是 boolean,而是 arg is Type

interface Admin { name: string; role: "admin"; email: string}
interface Guest { name: string; role: "guest"; }

type User = Admin | Guest;

// ✨ 关键语法:parameterName is Type
function isAdmin(user: User): user is Admin {
    return user.role === "admin";
}

function kickUser(user: User) {
    // if (user.role === "admin") { ... } // 这样也可以,但逻辑多了会乱
    
    // 使用自定义守卫
    if (isAdmin(user)) {
        // ✅ TS 知道这里是 Admin,可以访问 admin 特有的属性
        console.log(user.email);
    } else {
        // ✅ 这里自动推导为 Guest
        console.log(user.name);
    }
}

is 关键字的作用:它告诉 TS 编译器:“如果这个函数返回 true,请相信我,参数一定是这个类型。”


❓ 常见问题

Q1: 为什么我用了 if (obj) 判断,TS 还说它可能是 null?

A: 在 TS 的严格模式 (strictNullChecks) 下,直接 if (obj) 只能排除 nullundefined。如果变量类型是 string | null,这样是可以的。但如果类型比较复杂(比如 {} | null),有时 TS 会更谨慎。最稳妥的写法是 if (obj !== null && obj !== undefined) 或者简写为 if (obj != null)

Q2: typeof value === "object" 能用来判断数组吗?

A: 不能!在 JavaScript (以及 TS) 中,typeof [] 也是 "object"。如果你想区分数组和普通对象,请使用 Array.isArray(value),这也是一个标准的 Type Guard。

Q3: 什么是“判别式联合”?

A: 这是一种设计模式。我们在接口中设置一个共同的字面量属性(如 type: 'circle' | 'square'),然后在 switch 语句中根据这个属性进行收窄。这是 2026 年处理复杂状态流(如 Redux/Toolkit)的标配写法。


🎯 总结

类型守卫是连接“逻辑判断”与“类型系统”的桥梁。本文我们学习了:

  1. 信任编译器:利用控制流分析,少用 as,多用 if
  2. 内置工具typeofinstanceofin 是最常用的武器。
  3. 自定义守卫:使用 is 关键字处理复杂的类型判断逻辑。

🚀 下期预告:
我们学会了判断类型,但如果类型本身很复杂呢?比如“既要有 A 的属性,又要有 B 的属性”?
下一篇文章我们将深入 《联合类型与交叉类型的艺术》,玩转类型的组合魔法。

💬 互动环节:
你现在还在代码里到处写 as any 或者 ! 吗?看完本文是不是想回去重构一下了?
如果觉得文章让你避开了很多坑,请点赞👍、收藏⭐、关注👀,下期更精彩!

更多推荐