TS类型断言和类型守卫

目录

前言

[一、 类型断言 (Type Assertion)](#一、 类型断言 (Type Assertion))

[1. 基础类型断言](#1. 基础类型断言)

[2. 非空断言 (!)](#2. 非空断言 (!))

[3. 确定赋值断言 (!)](#3. 确定赋值断言 (!))

4.双重断言

5.常量断言 (as const)

[1. 基础类型的固定应用](#1. 基础类型的固定应用)

[2. 将数组转化为只读元组 (Tuple)](#2. 将数组转化为只读元组 (Tuple))

[3. 将对象转化为深度只读对象 (Readonly)](#3. 将对象转化为深度只读对象 (Readonly))

[4. 解决函数返回值解构时的类型丢失问题](#4. 解决函数返回值解构时的类型丢失问题)

[5. 将常量对象的值自动转成联合类型](#5. 将常量对象的值自动转成联合类型)

二、类型守卫

[1. 实例判断:instanceof](#1. 实例判断:instanceof)

[2. 属性判断:in](#2. 属性判断:in)

[3. 类型判断:typeof](#3. 类型判断:typeof)

[1. typeof 的局限性演示](#1. typeof 的局限性演示)

[2. 原型链方法:Object.prototype.toString.call()](#2. 原型链方法:Object.prototype.toString.call())

[3. 为什么在 TS 中直接使用会报错?(核心重点)](#3. 为什么在 TS 中直接使用会报错?(核心重点))

[4. 解决方法:结合「自定义类型守卫 (is)」](#4. 解决方法:结合「自定义类型守卫 (is)」)

[4. 字面量相等判断:===, !==, ==, !=](#4. 字面量相等判断:===, !==, ==, !=)

[5. 自定义守卫 (is 关键字)](#5. 自定义守卫 (is 关键字))


前言

在 TypeScript 开发中,我们经常会遇到编译器无法准确推断出变量具体类型,或者我们需要在复杂的联合类型中精确锁定某一个类型的场景。为了解决这些问题,TypeScript 提供了两大利器:"类型断言"和"类型守卫"。

一、 类型断言 (Type Assertion)

类型断言的核心思想是绕过 TypeScript 的编译检查,直接告诉编译器 :"在特定的环境中,我比你更清楚这个值具体是什么类型,你不需要再给我进行类型检查了,相信我"。

类型断言主要分为基础类型断言、非空断言、确定赋值断言,以及as const断言

1. 基础类型断言

当我们需要强制将一个类型转换为另一个类型时,可以使用基础类型断言。常见的语法有两种:

语法一as 语法,格式为 变量 as 数据类型

语法二:尖括号语法,格式为 <数据类型>变量

注意事项:在 React 开发中,尖括号语法会与 JSX 语法产生严重冲突并导致报错,因此在实际开发中,我们通常只使用 as 语法

vbnet 复制代码
function fun(n: string | number) {
    // 错误写法:直接调用 n.length 会报错,因为 n 有可能是 number,而 number 没有 length 属性 [1]。
    // let num = n.length; 

    // 正确写法:使用类型断言,明确告诉编译器此时 n 就是 string 类型 
    let num = (n as string).length;
    console.log("num," + num);
}
fun("hello");

2. 非空断言 (!)

在某些上下文中,类型检查器可能无法断定一个变量是否为空。此时,我们可以使用一个后缀表达式操作符 ! 来进行非空断言。它相当于向编译器保证:"这个对象绝对不是 null 或 undefined,请直接放行,别报错"。

vbnet 复制代码
const Info = (name: string | null | undefined) => {
    // let str: string = name; // 直接赋值会报错,因为 name 可能为 null/undefined
    
    // 使用非空断言 ! ,过滤掉 null 和 undefined 类型,编译器会默认这里只传来 string 类型的数据 [1, 2]。
    let str2: string = name!; 
    console.log(str2);
}
Info('Domesy');

3. 确定赋值断言 (!)

在 TypeScript 2.7 版本中引入了确定赋值断言。它允许我们在实例属性和变量声明的后面放置一个**!** 号。这等同于告诉编译器:"我保证这个变量在后续使用前一定会赋值,你不需要检查我有没有提前赋值"。

vbnet 复制代码
let num: number; // 普通声明
let num1!: number; // 确定赋值断言,变量名后加了 ! 
// 如果此时直接 console.log(num) 可能会因为未赋值被 TS 警告,但使用 num1! 则不会。

非空断言和确定断言的区别:

非空断言 使用位置:用于变量的使用/调用阶段,直接跟在变量名或表达式后面

确定断言 使用位置:用于变量声明或实例属性声明阶段,写在变量名和冒号之间(即 变量名!: 类型)


4.双重断言

1. 什么是双重断言与产生场景 在绝大多数情况下,基础的类型断言(如 变量 as 类型)就能满足我们的需求。但是,TypeScript 依然内置了安全底线:它不允许你将两个完全不兼容的类型进行直接断言。当基础断言失效时,我们可能就会用到双重断言,但需要明确的是,一般情况下并不推荐频繁使用它,因为它会破坏类型安全性。

失效的具体情况: TypeScript 规定,基础类型不能直接断言为不相关的接口类型。

2. 代码举例与原理解析 假设我们定义了一个描述用户信息的接口 Info,包含姓名和年龄。如果此时我们有一个普通的字符串变量,试图直接将其断言为 Info 接口,TypeScript 编译器会立刻抛出错误。

vbnet 复制代码
// 定义一个接口
interface Info {
    name: string;
    age: number;
} 

let str = "hello typescript";

// 报错尝试:基础类型直接断言为不相关的接口
// let user = str as Info; 
// 编译器会报错:类型 "string" 到类型 "Info" 的转换可能是错误的,因为两种类型不能充分重叠。

// 解决方案:使用双重断言
// 原理:先将变量断言为一个极其宽泛的类型(如 any 或 unknown),然后再将其断言为我们想要的目标类型。
let user = str as any as Info; 

console.log(user.name); // 此时编译器不再报错,成功绕过了极其严格的类型校验

此时虽然双重断言能够解决编译器的报错,但在运行时 user 变量本质上依然是一个字符串,如果直接去访问 user.name 还是会得到 undefined


5.常量断言 (as const)

as const 是 TypeScript 中最严格、也最实用的断言方式之一。它的核心作用是将变量变成"只读 + 最精确的字面量类型",强制要求类型不被拓宽,也不允许被任何人修改

1. 基础类型的固定应用

对于普通的字符串或数字,如果我们用 let 声明,TypeScript 会将其类型推断为宽泛的 string 或 number,这意味着变量的值可以被随意更改。

vbnet 复制代码
// 普通声明:类型被推断为 string,可以随意改成其他字符串
let a = 'hello'; 
a = 'world'; // 正常运行

// 使用 as const 断言
let b = 'hello' as const; // 类型被彻底固定死为字面量类型 'hello' 
// b = 'world'; // 报错:不能将类型""world""分配给类型""hello"" 
2. 将数组转化为只读元组 (Tuple)

在不使用 as const 的情况下,数组的元素是可以被随意追加(push)或修改的。使用后,整个数组将变成深度的只读状态。

vbnet 复制代码
// 普通数组:类型推断为 number[]
const list = [1-3]; 
list.push(4); // 正常运行

// 使用 as const 断言
const listConst = [1-3] as const; // 类型变为极度精确的 readonly [1-3] [1], [3]
// listConst.push(4); // 报错:类型"readonly [1-3]"上不存在属性"push" [3]
// listConst = 10; // 报错:无法分配到 "0" ,因为它是只读属性 
3. 将对象转化为深度只读对象 (Readonly)

当需要保护一个配置对象不被外界篡改时,as const 是比 Object.freeze 在类型层面更彻底的解决方案。

vbnet 复制代码
// 使用 as const 保护对象
let user2 = { 
    name: 'tom', 
    age: 18 
} as const; 

// 对象的每一个属性都被加上了 readonly 修饰符
// user2.name = 'jack'; // 报错,属性只读,不能修改 
4. 解决函数返回值解构时的类型丢失问题

当一个函数返回一个包含不同类型元素的数组时,如果不做处理,TypeScript 会将解构出来的变量推断为联合类型的数组。借助 as const,我们可以完美保持类型的精确性。

vbnet 复制代码
function ew() {
    let str: string = "hello";
    let fun = (a: number, b: number): number => a + b;
    
    // 如果直接 return [str, fun]; 解构后 bb 的类型会是 string | Function,直接调用 bb() 会报错 
    
    // 解决方案:直接在返回值里使用 as const 断言为只读元组
    return [str, fun] as const; 
    
    // 补充:除此之外,也可以使用其他断言写法来达到相同目的:
    // return [str, fun] as [string, Function] 
    // return [str, fun] as [typeof str, typeof fun] 
}

// 解构使用
let [aa, bb] = ew(); 
// 此时 TS 明确知道 bb 是一个函数,可以安全调用
let res = bb(10, 20); 
console.log(res); [3]
5. 将常量对象的值自动转成联合类型

在状态码管理或枚举场景中,我们经常需要把一个常量对象的 value 提取出来,生成一个联合类型,限制传参的范围。as const 配合内置操作符可以极简地实现这一点。

vbnet 复制代码
// 1. 使用 as const 定义固定值类型的常量对象
export const STATUS = {
    TODO: 0,
    DOING: 1,
    DONE: 2
} as const; 

// 2. 提取类型的核心推导过程:
// - typeof STATUS:拿到该对象的精确类型结构 
// - keyof typeof STATUS:拿到所有键的联合类型,即 "TODO" | "DOING" | "DONE" 
// - 最终通过对象取值方式拿到所有值的联合类型 
type StatusType = typeof STATUS[keyof typeof STATUS]; 

// StatusType 的最终结果被精确推导为:0 | 1 | 2 

通过上述代码,后续如果有函数需要接收状态码,将其参数类型定义为 StatusType,就可以确保调用方只能传入 0、1 或 2,极大地提升了系统的类型安全性。


二、类型守卫

定义与产生时机:类型守卫是指在语句的块级作用域(例如if语句内或条件运算符表达式内),通过特定的条件关键字,缩小变量潜在类型范围的一种类型推断行为。它可以帮助我们在特定的代码块中,获得更为安全、精确的变量类型。

1. 实例判断:instanceof

JavaScript 中的 instanceof 运算符用于检查一个值的原型链中是否含有另一个值的 prototype。TypeScript 考虑到了这一点,在由 instanceof 保护的 if 分支中,会自动缩小类型范围

vbnet 复制代码
function logValue(x: Date | string) {
    if (x instanceof Date) {
        // 在这个块级作用域内,TS 明确知道 x 是 Date 实例,因此安全调用 toUTCString() 
        console.log(x.toUTCString());
    } else {
        // 既然不是 Date,那一定只剩下 string 类型,可以安全调用 toUpperCase() 
        console.log(x.toUpperCase());
    }
}
logValue(new Date()); // 
logValue("hello ts"); // 

2. 属性判断:in

in 运算符用于确定对象是否具有某个名称的属性。TypeScript 会根据 in 的判断结果(true 或 false 的分支)来缩小潜在对象的类型范围

vbnet 复制代码
type Fish = { swim: () => void } 
type Bird = { fly: () => void } 

function move(animal: Fish | Bird) {
    if ("swim" in animal) {
        // 如果具有 swim 属性,TS 推断 animal 是 Fish,可以调用 swim()
        animal.swim();
    } else {
        // 否则推断为 Bird
        animal.fly();
    }
}

3. 类型判断:typeof

typeof 守卫用来检测一个变量的数据类型,其检测范围包括:"string"、"number"、"bigint"、"boolean"、"symbol"、"undefined"、"object"、"function" 等

vbnet 复制代码
function fun(n: string | number) {
    let num: number;
    // 使用 typeof 守卫缩小类型
    if (typeof n == 'string') {
        num = n.length; // 此时调用 n.length 绝对安全 
        console.log("num," + num);
    }
}

注意 typeof 的局限性 :在类型判断中,typeof 守卫非常常用,它能够有效检测出 "string"、"number"、"boolean"、"function" 等基础数据类型。但是,typeof 存在一个致命的局限性:当它面对复杂引用数据类型(如 Array、Object、Map、Set)时,都会统一返回**"object"**,这导致我们完全无法精确区分它们

1. typeof 的局限性演示
vbnet 复制代码
const arr = ;
const set = new Set();
const map = new Map();

console.log(typeof arr); // 输出 "object" 
console.log(typeof set); // 输出 "object" 
console.log(typeof map); // 输出 "object" 
2. 原型链方法:Object.prototype.toString.call()

为了精确区分这些类型,在 JavaScript 的底层原理中,我们通常会借用 Object.prototype.toString.call() 方法,它可以打印出对象内部的 [[Class]] 标签,从而实现精准识别。

vbnet 复制代码
console.log(Object.prototype.toString.call(arr)); // 精确输出: [object Array] 
console.log(Object.prototype.toString.call(set)); // 精确输出: [object Set] 
console.log(Object.prototype.toString.call(map)); // 精确输出: [object Map] 
3. 为什么在 TS 中直接使用会报错?(核心重点)

很多开发者在 TS 中会这样写:

vbnet 复制代码
function processData(data: string | any[]) {
    // 报错场景分析:
    if (Object.prototype.toString.call(data) === '[object Array]') {
        // 此时如果你调用 data.push(),TS 可能会报错!
        // 因为 TS 的类型推断系统默认不认识这种字符串比对,它不知道此时 data 已经变成了数组。
    }
}
4. 解决方法:结合「自定义类型守卫 (is)」

为了解决上面的报错问题,我们需要把 Object.prototype.toString.call() 封装进一个自定义类型守卫中 。通过is关键字明确告诉 TypeScript 编译器:"只要这个函数返回 true,传入的参数就绝对是这个具体的类型"。

完整且不会报错的代码示例:

vbnet 复制代码
// 1. 封装检测数组的自定义类型守卫 
// 注意这里的返回值类型是 val is any[],这才是让 TS 不报错的关键 
function isArrayGuard(val: any): val is any[] {
    return Object.prototype.toString.call(val) === '[object Array]';
}

// 2. 封装检测 Set 的自定义类型守卫
function isSetGuard(val: any): val is Set<any> {
    return Object.prototype.toString.call(val) === '[object Set]';
}

// 3. 在业务逻辑中安全使用
function processComplexData(data: string | any[] | Set<any>) {
    
    // 使用我们自定义的 isArrayGuard
    if (isArrayGuard(data)) {
        // 此时 TS 明确知道 data 是 any[] 数组类型
        // 我们可以安全地调用数组方法,绝不报错
        data.push("新元素"); 
        console.log("处理数组:", data.length);
        
    } else if (isSetGuard(data)) {
        // 此时 TS 明确知道 data 是 Set 类型
        data.add("新元素");
        console.log("处理Set:", data.size);
        
    } else {
        // 排除以上两者,TS 自动推断此时 data 一定是 string 类型
        console.log("处理字符串:", data.toUpperCase());
    }
}

// 测试执行
processComplexData([5-7]);
processComplexData(new Set([5, 6]));

4. 字面量相等判断:===, !==, ==, !=

在条件语句中,使用严格相等或不相等可以极大地帮助 TypeScript 缩小类型范围。

vbnet 复制代码
function printAll(strs: string | string[] | null) {
    if (strs !== null) { // 守卫一:过滤掉 null,此时 strs 缩小为 string | string[]
        if (typeof strs === "object") { // 守卫二:过滤出数组(在 JS 中 typeof 数组为 object)[6]。
            for (const s of strs) {
                console.log(s);
            }
        } else if (typeof strs === "string") { // 守卫三:明确为字符串
            console.log(strs);
        }
    }
}

再比如过滤 undefined:

vbnet 复制代码
interface Container { value: number | null | undefined; } 
function multiplyValue(container: Container, factor: number) {
    if (container.value != null) { // 巧妙利用 != null 同时排除了 null 和 undefined 
        container.value *= factor; // 此处 value 被精确推断为 number 
    }
}

5. 自定义守卫 (is 关键字)

当内置的守卫无法满足复杂的业务逻辑时,我们可以通过编写返回布尔值的函数来自定义类型守卫

自定义守卫的格式为:function 函数名(形参: any): 形参 is A类型 { return true or false }

它的核心意义在于:让 TS 明确知道 if/else里的变量到底是什么类型,从而安全地调用属性而不报错

vbnet 复制代码
// 定义一个自定义守卫:返回值为布尔值,且明确告知 TS 如果返回 true,num 就是 number 类型 
function isNum(num: any): num is number {
    return typeof num === 'number'; 
}

function fun2(num: string | number) {
    if (isNum(num)) {
        // 由于自定义守卫生效,TS 在这里将 num 明确当作 number 处理 
        console.log(num);
    } else {
        // 在 else 分支,TS 知道排除了 number,这里 num 一定是 string 类型,安全调用 length
        console.log(num.length);
    }
}
相关推荐
木斯佳2 小时前
前端八股文面经大全:京东前端实习一面(2026-04-16)·面经深度解析
前端
chenxu98b2 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
Bigger2 小时前
第十章:我是如何剖析 CLI 里的终极 Agent 能力的(电脑控制与浏览器接管)
前端·claude·源码阅读
kyriewen2 小时前
代码写成一锅粥?这5种设计模式让你的项目“起死回生”
前端·javascript·设计模式
蓝色的雨2 小时前
基于Babylonjs的WEBGPU渲染器源码架构
前端·javascript
浇头面加面2 小时前
📊 流式输出实现总结
前端
IT_陈寒2 小时前
Java集合的这个坑,我调试了整整3小时才爬出来
前端·人工智能·后端
前端老石人3 小时前
前端网站换肤功能的 3 种实现方案
开发语言·前端·css·html
冴羽yayujs3 小时前
2026 年的 JavaScript 已经不是你认识的 JavaScript 了
前端·javascript