TS类型断言和类型守卫
目录前言一、 类型断言 (Type Assertion)1. 基础类型断言2. 非空断言 (!)3. 确定赋值断言 (!)4.双重断言5.常量断言 (as const)1. 基础类型的固定应用2. 将数组转化为只读元组 (Tuple)3. 将对象转化为深度只读对象 (Readonly)4. 解决函数返回值解构时的类型丢失问题5. 将常量对象的值自动转成联合类型二、类型守卫1. 实例判断instanceof2. 属性判断in3. 类型判断typeof1. typeof 的局限性演示2. 原型链方法Object.prototype.toString.call()3. 为什么在 TS 中直接使用会报错核心重点4. 解决方法结合「自定义类型守卫 (is)」4. 字面量相等判断, !, , !5. 自定义守卫 (is 关键字)前言在 TypeScript 开发中我们经常会遇到编译器无法准确推断出变量具体类型或者我们需要在复杂的联合类型中精确锁定某一个类型的场景。为了解决这些问题TypeScript 提供了两大利器“类型断言”和“类型守卫”。一、 类型断言 (Type Assertion)类型断言的核心思想是绕过 TypeScript 的编译检查直接告诉编译器“在特定的环境中我比你更清楚这个值具体是什么类型你不需要再给我进行类型检查了相信我”。类型断言主要分为基础类型断言、非空断言、确定赋值断言以及as const断言1. 基础类型断言当我们需要强制将一个类型转换为另一个类型时可以使用基础类型断言。常见的语法有两种语法一as 语法格式为 变量 as 数据类型语法二尖括号语法格式为 数据类型变量注意事项在 React 开发中尖括号语法会与 JSX 语法产生严重冲突并导致报错因此在实际开发中我们通常只使用 as 语法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请直接放行别报错”。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 版本中引入了确定赋值断言。它允许我们在实例属性和变量声明的后面放置一个!号。这等同于告诉编译器“我保证这个变量在后续使用前一定会赋值你不需要检查我有没有提前赋值”。let num: number; // 普通声明 let num1!: number; // 确定赋值断言变量名后加了 ! // 如果此时直接 console.log(num) 可能会因为未赋值被 TS 警告但使用 num1! 则不会。非空断言和确定断言的区别非空断言使用位置用于变量的使用/调用阶段直接跟在变量名或表达式后面确定断言使用位置用于变量声明或实例属性声明阶段写在变量名和冒号之间即 变量名!: 类型4.双重断言1. 什么是双重断言与产生场景在绝大多数情况下基础的类型断言如 变量 as 类型就能满足我们的需求。但是TypeScript 依然内置了安全底线它不允许你将两个完全不兼容的类型进行直接断言。当基础断言失效时我们可能就会用到双重断言但需要明确的是一般情况下并不推荐频繁使用它因为它会破坏类型安全性。失效的具体情况 TypeScript 规定基础类型不能直接断言为不相关的接口类型。2. 代码举例与原理解析假设我们定义了一个描述用户信息的接口 Info包含姓名和年龄。如果此时我们有一个普通的字符串变量试图直接将其断言为 Info 接口TypeScript 编译器会立刻抛出错误。// 定义一个接口 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 还是会得到 undefined5.常量断言 (as const)as const 是 TypeScript 中最严格、也最实用的断言方式之一。它的核心作用是将变量变成“只读 最精确的字面量类型”强制要求类型不被拓宽也不允许被任何人修改1. 基础类型的固定应用对于普通的字符串或数字如果我们用 let 声明TypeScript 会将其类型推断为宽泛的 string 或 number这意味着变量的值可以被随意更改。// 普通声明类型被推断为 string可以随意改成其他字符串 let a hello; a world; // 正常运行 // 使用 as const 断言 let b hello as const; // 类型被彻底固定死为字面量类型 hello // b world; // 报错不能将类型“world”分配给类型“hello”2. 将数组转化为只读元组 (Tuple)在不使用 as const 的情况下数组的元素是可以被随意追加push或修改的。使用后整个数组将变成深度的只读状态。// 普通数组类型推断为 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 在类型层面更彻底的解决方案。// 使用 as const 保护对象 let user2 { name: tom, age: 18 } as const; // 对象的每一个属性都被加上了 readonly 修饰符 // user2.name jack; // 报错属性只读不能修改4. 解决函数返回值解构时的类型丢失问题当一个函数返回一个包含不同类型元素的数组时如果不做处理TypeScript 会将解构出来的变量推断为联合类型的数组。借助 as const我们可以完美保持类型的精确性。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 配合内置操作符可以极简地实现这一点。// 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. 实例判断instanceofJavaScript 中的 instanceof 运算符用于检查一个值的原型链中是否含有另一个值的 prototype。TypeScript 考虑到了这一点在由 instanceof 保护的 if 分支中会自动缩小类型范围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. 属性判断inin 运算符用于确定对象是否具有某个名称的属性。TypeScript 会根据 in 的判断结果true 或 false 的分支来缩小潜在对象的类型范围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. 类型判断typeoftypeof 守卫用来检测一个变量的数据类型其检测范围包括string、number、bigint、boolean、symbol、undefined、object、function 等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 的局限性演示const arr ; const set new Set(); const map new Map(); console.log(typeof arr); // 输出 object console.log(typeof set); // 输出 object console.log(typeof map); // 输出 object2. 原型链方法Object.prototype.toString.call()为了精确区分这些类型在 JavaScript 的底层原理中我们通常会借用 Object.prototype.toString.call() 方法它可以打印出对象内部的 [[Class]] 标签从而实现精准识别。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 中会这样写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传入的参数就绝对是这个具体的类型”。完整且不会报错的代码示例// 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 Setany { return Object.prototype.toString.call(val) [object Set]; } // 3. 在业务逻辑中安全使用 function processComplexData(data: string | any[] | Setany) { // 使用我们自定义的 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 缩小类型范围。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); } } }再比如过滤 undefinedinterface 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里的变量到底是什么类型从而安全地调用属性而不报错// 定义一个自定义守卫返回值为布尔值且明确告知 TS 如果返回 truenum 就是 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); } }