Skip to content

类型断言、类型守卫与二者对比

前两篇为专题,第三篇为对照表与总结。


第一部分:类型断言

类型断言是手动指定变量类型的语法,仅作用于编译阶段,不改变运行时类型

作用

核心作用是让你手动告诉编译器某个值的具体类型,覆盖它的默认类型推断。

语法

  • 语法一:尖括号 语法(<类型>值)

这是早期的语法形式,在 JSX 中会与标签语法冲突(因此 JSX 中不推荐使用)

ts
// 示例:把一个不确定类型的变量断言为字符串
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
console.log(strLength); // 输出:16
  • 语法二:as 语法(值 as 类型)

官方推荐的语法,兼容 JSX 场景,是目前最常用的写法

ts
// 示例:把一个不确定类型的变量断言为字符串
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
console.log(strLength); // 输出:16
  • as const 常量断言:将字面量收窄为只读字面量类型,例如 const x = [1, 2] as const

  • 非空断言 !(慎用):写在表达式后缀,如 document.getElementById('id')!,表示断言此处不是 null / undefined不是 value! as Type 这种写法。

ts
// 非空断言示例(确定 DOM 存在时)
const el = document.getElementById('app')!;
el.innerHTML = 'ok';
shell
# const 断言(as const)
# 将值的类型收窄为字面量联合,并把对象/元组标为 readonly
语法:值 as const

使用场景

  • 场景一:DOM 元素断言(最常用的实战场景)
ts
// 原因:TS 对 DOM 元素的默认类型是宽泛的(如 HTMLElement/null),需要断言为具体的元素类型(如 HTMLInputElement)。

// 获取输入框元素,默认类型是 HTMLElement | null
const input = document.getElementById("username");

// 断言为 HTMLInputElement(排除 null 需额外判断)(相比非空断言更安全)
if (input) {
  console.log((input as HTMLInputElement).value); // 访问 input 的 value 属性
}

// 简化写法(结合非空断言 !,仅确定元素一定存在时使用)
const input2 = document.getElementById("username")! as HTMLInputElement;
console.log(input2.value);
  • 场景二:收窄类型
ts
// 原因:当变量类型是宽泛的(如 any/unknown/ 联合类型),需要缩小为更具体的类型时使用。

// 1. unknown 类型断言为具体类型
function getLength(value: unknown): number {
  // 先断言为 string,再访问 length 属性
  if (typeof value === "string") {
    return (value as string).length;
  }
  // 断言为数组,访问 length 属性
  if (Array.isArray(value)) {
    return (value as any[]).length;
  }
  return 0;
}

console.log(getLength("hello")); // 5
console.log(getLength([1, 2, 3])); // 3

// 2. 联合类型断言为其中一种类型
type MyType = string | number;
function printStr(str: MyType) {
  // 断言为 string,调用字符串方法
  console.log((str as string).toUpperCase());
}
printStr("typescript"); // TYPESCRIPT
  • 场景三:“双重断言”(谨慎使用)
ts
// 案例:当需要跨类型断言(如基本类型 → 对象类型),TS 会禁止直接断言,此时需要通过 any/unknown 作为中间层(双重断言)。
// 原因:容易掩盖真实的类型错误,非必要不使用。

/// 直接断言会报错:类型 'string' 不能转换为类型 'number[]'
// let numArr = "123" as number[];

// 双重断言(先断言为 any,再断言为目标类型)
let numArr = "123" as any as number[];
console.log(numArr); // 输出:"123"(实际类型仍为字符串)
  • 场景四:“常量断言”
ts
// 普通对象:属性可修改,类型是 { name: string; age: number }
const user = { name: "张三", age: 20 };
user.age = 21; // 可以修改

// const 断言:对象变为 readonly,属性类型固化为字面量
const userConst = { name: "张三", age: 20 } as const;
// userConst.age = 21; // 报错:无法分配到 "age",因为它是只读属性

// 类型:readonly { readonly name: "张三"; readonly age: 20; }
type UserConstType = typeof userConst;

注意事项

  • 类型断言是 TS 编译时的类型提示,不影响运行时逻辑

  • 类型断言不是类型转换:它只影响编译阶段的类型检查,运行时不会做任何类型转换。如果断言错误,运行时可能报错

ts
let num: any = 123;
// 错误断言:num 实际是数字,运行时调用 split 会报错
const str = num as string;
str.split(""); // 运行时错误:num.split is not a function
  • 断言的限制:两个类型之间没有直接继承或兼容关系时,使用断言会报错,需要双重断言
shell
# 解决方案:双重断言(如果非要强制断言(不推荐),可以通过 any 中转)
可以先断言成 any 或者 unknown,这样可以绕过类型检查,再断言成目标类型
语法:变量值 as any as 目标类型

第二部分:类型守卫

类型守卫是一段运行时检查的代码

它的作用是在特定代码块内「缩小」变量的类型范围(类型窄化)

让 TypeScript 编译器能够精确推断出变量的具体类型,从而避免类型错误,同时获得更好的类型提示。

没有类型守卫时,TS 只知道变量是宽泛的联合类型;有了类型守卫后,TS 能确定变量在当前分支下的具体类型。

应用场景

  • 处理联合类型:这是最核心的场景,避免在联合类型上调用不存在的方法 / 属性;
  • 处理可选值 / 空值:结合 typeof value !== 'undefined' 或 value !== null 窄化非空类型;
  • 处理接口 / 类的多态:区分不同接口 / 类的实例,调用各自的方法;
  • 运行时类型校验:弥补 TypeScript 编译后类型擦除的问题,确保运行时数据符合预期。

种类

基础场景用 typeof / instanceof / in,复杂场景用自定义类型守卫(is 关键字)

  1. typeof 检查基础类型:适用于检查原始类型(string / number / boolean / symbol / undefined / bigint / function),是最基础的类型守卫。
  2. instanceof 检查类/实例:适用于检查对象是否是某个类的实例(包括内置类如 Array / Date,或自定义类),依赖原型链判断。
  3. in 检查对象的属性:适用于检查对象是否包含某个特定属性,从而窄化联合类型中对象的类型。
  4. 自定义类型守卫:通过 is 关键字明确窄化后的类型。
  5. 字面量类型守卫:const 断言 / 等值判断,适用于字面量类型(如 'success' | 'error' | 'loading'),通过等值判断(===/!==)窄化类型。

typeof 检查基础类型

ts
function printValue(value: string | number | boolean) {
  // 类型守卫:检查 value 是否为 string 类型
  if (typeof value === 'string') {
    // 此处 TS 能确定 value 是 string 类型,可安全调用 string 方法
    console.log('字符串长度:', value.length);
  }
  // 类型守卫:检查 value 是否为 number 类型
  else if (typeof value === 'number') {
    // 此处 TS 能确定 value 是 number 类型,可安全进行数值运算
    console.log('数值翻倍:', value * 2);
  }
  // 剩余分支自动推断为 boolean 类型
  else {
    console.log('布尔值取反:', !value);
  }
}

// 测试
printValue('Hello'); // 输出:字符串长度: 5
printValue(100); // 输出:数值翻倍: 200
printValue(true); // 输出:布尔值取反: false

instanceof 检查类/实例

ts
class Dog {
  bark() {
    console.log('汪汪汪');
  }
}

class Cat {
  meow() {
    console.log('喵喵喵');
  }
}

function petSay(pet: Dog | Cat) {
  // 类型守卫:检查 pet 是否是 Dog 的实例
  if (pet instanceof Dog) {
    pet.bark(); // 此处 TS 确定 pet 是 Dog 类型
  } else {
    pet.meow(); // 此处 TS 确定 pet 是 Cat 类型
  }
}

// 测试
petSay(new Dog()); // 输出:汪汪汪
petSay(new Cat()); // 输出:喵喵喵

in 检查对象的属性

ts
interface User {
  name: string;
  login: () => void;
}

interface Guest {
  name: string;
  register: () => void;
}

function handleUser(user: User | Guest) {
  // 类型守卫:检查 user 是否包含 login 属性
  if ('login' in user) {
    user.login(); // 此处 TS 确定 user 是 User 类型
  } else {
    user.register(); // 此处 TS 确定 user 是 Guest 类型
  }
}

// 测试
handleUser({ name: '张三', login: () => console.log('登录成功') }); // 输出:登录成功
handleUser({ name: '游客', register: () => console.log('注册成功') }); // 输出:注册成功

自定义类型守卫

ts
// 定义联合类型
type NumArray = number[];
type StrArray = string[];
type MixedArray = NumArray | StrArray;

// 自定义类型守卫:判断数组是否为数字数组
function isNumArray(arr: MixedArray): arr is NumArray {
  // 运行时检查:数组中所有元素都是数字
  return arr.every((item) => typeof item === 'number');
}

function sumArray(arr: MixedArray) {
  // 使用自定义类型守卫
  if (isNumArray(arr)) {
    // 此处 TS 确定 arr 是 NumArray 类型,可安全求和
    return arr.reduce((sum, num) => sum + num, 0);
  } else {
    // 此处 TS 确定 arr 是 StrArray 类型,拼接字符串
    return arr.join(', ');
  }
}

// 测试
console.log(sumArray([1, 2, 3])); // 输出:6
console.log(sumArray(['a', 'b', 'c'])); // 输出:a, b, c

字面量类型守卫

ts
type Status = 'success' | 'error' | 'loading';

function handleStatus(status: Status) {
  // 类型守卫:等值判断
  if (status === 'success') {
    console.log('操作成功'); // 此处 status 是 'success' 字面量类型
  } else if (status === 'error') {
    console.log('操作失败'); // 此处 status 是 'error' 字面量类型
  } else {
    console.log('加载中...'); // 此处 status 是 'loading' 字面量类型
  }
}

// 测试
handleStatus('loading'); // 输出:加载中...

第三部分:断言与守卫对比

对比

shell
维度       类型断言                             类型守卫
本质       手动覆盖类型(编译期语法)            运行时检查类型(代码逻辑)
类型安全    不安全(编译器信任你的断言,无校验) 安全(基于运行时检查缩小类型范围)
运行时影响 无(仅编译期生效)                  有(执行检查逻辑)
类型推导    强制指定类型,无自动推导            编译器自动推导窄类型
适用场景    明确知道类型,且无法通过逻辑推导       需要动态判断类型,保证类型安全

总结

  1. 类型断言是 “硬指定” 类型,仅编译期生效,适合你100% 确定类型的场景(如 DOM 元素获取),但要谨慎使用,避免运行时错误
  2. 类型守卫是 “动态推导” 类型,通过运行时检查缩小类型范围,是保证类型安全的首选方式,适合需要分支判断类型的场景
  3. 核心原则:能使用类型守卫的场景,尽量不用类型断言;只有当你明确知道类型且无法通过逻辑推导时,才考虑类型断言