目录
[一、 类型断言 (Type Assertion)](#一、 类型断言 (Type Assertion))
[1. 基础类型断言](#1. 基础类型断言)
[2. 非空断言 (!)](#2. 非空断言 (!))
[3. 确定赋值断言 (!)](#3. 确定赋值断言 (!))
[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);
}
}