类型守卫
概述
类型守卫(Type Guards)是 TypeScript 中一种强大的类型缩小(Type Narrowing)机制,它允许你在运行时检查值的类型,并让 TypeScript 编译器在相应的代码块中自动推断出更具体的类型。类型守卫通过运行时检查来确保类型安全,比类型断言更安全可靠。TypeScript 支持多种类型守卫方式:typeof 操作符、instanceof 操作符、in 操作符,以及自定义类型守卫函数。类型守卫是处理联合类型、处理 unknown 类型、确保类型安全的最佳实践。
基本概念
类型守卫的核心思想是:通过运行时检查来缩小类型范围。当 TypeScript 编译器遇到类型守卫时,它会在相应的代码块中将类型缩小为更具体的类型。
typescript
// 联合类型
type StringOrNumber = string | number;
function process(value: StringOrNumber): void {
// 使用 typeof 类型守卫
if (typeof value === "string") {
// 在这个代码块中,TypeScript 知道 value 是 string
console.log(value.toUpperCase()); // ✅ 可以安全调用
} else {
// 在这个代码块中,TypeScript 知道 value 是 number
console.log(value.toFixed(2)); // ✅ 可以安全调用
}
}typeof 类型守卫
typeof 操作符是最常用的类型守卫之一,用于检查基本类型。
基本用法
typescript
// 使用 typeof 检查基本类型
function processValue(value: string | number | boolean): void {
if (typeof value === "string") {
// TypeScript 知道这里是 string
console.log(value.length);
console.log(value.toUpperCase());
} else if (typeof value === "number") {
// TypeScript 知道这里是 number
console.log(value.toFixed(2));
console.log(value * 2);
} else {
// TypeScript 知道这里是 boolean
console.log(value ? "真" : "假");
}
}
// 使用示例
processValue("hello"); // 输出: 5, HELLO
processValue(42); // 输出: 42.00, 84
processValue(true); // 输出: 真typeof 支持的类型
typeof 可以检查以下类型:
typescript
function checkType(value: unknown): void {
if (typeof value === "string") {
// string
} else if (typeof value === "number") {
// number
} else if (typeof value === "boolean") {
// boolean
} else if (typeof value === "undefined") {
// undefined
} else if (typeof value === "object") {
// object(注意:null 也是 "object")
} else if (typeof value === "function") {
// function
} else if (typeof value === "symbol") {
// symbol
} else if (typeof value === "bigint") {
// bigint
}
}注意
typeof null 返回 "object",这是 JavaScript 的历史遗留问题。要检查 null,应该直接使用 value === null 或 value !== null。
处理 null 和 undefined
typescript
// 检查 null
function processNullable(value: string | null): void {
if (value === null) {
// TypeScript 知道这里是 null
console.log("值为 null");
} else {
// TypeScript 知道这里是 string
console.log(value.toUpperCase());
}
}
// 检查 undefined
function processOptional(value: string | undefined): void {
if (value === undefined) {
// TypeScript 知道这里是 undefined
console.log("值为 undefined");
} else {
// TypeScript 知道这里是 string
console.log(value.length);
}
}
// 同时检查 null 和 undefined
function processMaybe(value: string | null | undefined): void {
if (value == null) { // 使用 == 可以同时检查 null 和 undefined
// TypeScript 知道这里是 null | undefined
console.log("值为 null 或 undefined");
} else {
// TypeScript 知道这里是 string
console.log(value.toUpperCase());
}
}
// 或者使用非空断言(如果确定不为空)
function processDefinite(value: string | null | undefined): void {
if (value != null) { // 使用 != 可以同时检查 null 和 undefined
// TypeScript 知道这里是 string
console.log(value.toUpperCase());
}
}instanceof 类型守卫
instanceof 操作符用于检查对象是否是某个类的实例。
基本用法
typescript
// 定义类
class Dog {
name: string;
breed: string;
constructor(name: string, breed: string) {
this.name = name;
this.breed = breed;
}
bark(): void {
console.log("Woof!");
}
}
class Cat {
name: string;
color: string;
constructor(name: string, color: string) {
this.name = name;
this.color = color;
}
meow(): void {
console.log("Meow!");
}
}
// 使用 instanceof 类型守卫
function handlePet(pet: Dog | Cat): void {
if (pet instanceof Dog) {
// TypeScript 知道这里是 Dog
pet.bark(); // ✅ 可以调用
console.log(pet.breed); // ✅ 可以访问 breed
} else {
// TypeScript 知道这里是 Cat
pet.meow(); // ✅ 可以调用
console.log(pet.color); // ✅ 可以访问 color
}
}
// 使用示例
const dog = new Dog("Buddy", "Golden Retriever");
const cat = new Cat("Whiskers", "Orange");
handlePet(dog); // 输出: Woof!, Golden Retriever
handlePet(cat); // 输出: Meow!, Orange处理内置类型
typescript
// 处理 Date 对象
function processDate(value: Date | string): void {
if (value instanceof Date) {
// TypeScript 知道这里是 Date
console.log(value.getFullYear());
console.log(value.toISOString());
} else {
// TypeScript 知道这里是 string
console.log(value.toUpperCase());
}
}
// 处理数组
function processArray(value: unknown): void {
if (value instanceof Array) {
// TypeScript 知道这里是 Array
console.log(value.length);
console.log(value.map(x => x));
} else {
// TypeScript 不知道具体类型
console.log("不是数组");
}
}
// 处理正则表达式
function processRegex(value: RegExp | string): void {
if (value instanceof RegExp) {
// TypeScript 知道这里是 RegExp
console.log(value.test("test"));
console.log(value.source);
} else {
// TypeScript 知道这里是 string
console.log(value.length);
}
}in 操作符类型守卫
in 操作符用于检查对象是否包含某个属性。
基本用法
typescript
// 定义接口
interface Dog {
name: string;
breed: string;
bark(): void;
}
interface Cat {
name: string;
color: string;
meow(): void;
}
// 使用 in 操作符检查属性
function handleAnimal(animal: Dog | Cat): void {
if ("breed" in animal) {
// TypeScript 知道这里是 Dog(因为有 breed 属性)
animal.bark(); // ✅ 可以调用
console.log(animal.breed);
} else {
// TypeScript 知道这里是 Cat(因为没有 breed 属性)
animal.meow(); // ✅ 可以调用
console.log(animal.color);
}
}
// 使用示例
const dog: Dog = {
name: "Buddy",
breed: "Golden Retriever",
bark: () => console.log("Woof!")
};
const cat: Cat = {
name: "Whiskers",
color: "Orange",
meow: () => console.log("Meow!")
};
handleAnimal(dog); // 输出: Woof!, Golden Retriever
handleAnimal(cat); // 输出: Meow!, Orange处理可选属性
typescript
interface User {
id: number;
name: string;
email?: string; // 可选属性
}
function processUser(user: User): void {
if ("email" in user) {
// TypeScript 知道 user 有 email 属性(但可能是 undefined)
if (user.email) {
// 进一步检查确保不是 undefined
console.log(user.email.toUpperCase());
}
} else {
// TypeScript 知道 user 没有 email 属性
console.log("用户没有邮箱");
}
}自定义类型守卫
自定义类型守卫是使用类型谓词(Type Predicate)的函数,格式为 parameterName is Type。
基本语法
typescript
// 自定义类型守卫函数
function isString(value: unknown): value is string {
return typeof value === "string";
}
// 使用自定义类型守卫
function process(value: unknown): void {
if (isString(value)) {
// TypeScript 知道这里是 string
console.log(value.toUpperCase());
console.log(value.length);
} else {
// TypeScript 不知道具体类型
console.log("不是字符串");
}
}类型谓词语法
类型守卫函数必须返回类型谓词(parameterName is Type):
typescript
// ✅ 正确:返回类型谓词
function isNumber(value: unknown): value is number {
return typeof value === "number";
}
// ❌ 错误:只返回 boolean
function isNumberWrong(value: unknown): boolean {
return typeof value === "number";
// 这样 TypeScript 不会进行类型缩小
}复杂类型守卫示例
typescript
// 定义类型
interface User {
id: number;
name: string;
email: string;
}
interface Admin {
id: number;
name: string;
role: "admin";
permissions: string[];
}
// 自定义类型守卫:检查是否为 User
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"email" in value &&
typeof (value as any).id === "number" &&
typeof (value as any).name === "string" &&
typeof (value as any).email === "string"
);
}
// 自定义类型守卫:检查是否为 Admin
function isAdmin(value: unknown): value is Admin {
if (!isUser(value)) {
return false;
}
return (
"role" in value &&
"permissions" in value &&
(value as any).role === "admin" &&
Array.isArray((value as any).permissions)
);
}
// 使用自定义类型守卫
function processUser(value: unknown): void {
if (isAdmin(value)) {
// TypeScript 知道这里是 Admin
console.log(`管理员: ${value.name}`);
console.log(`权限: ${value.permissions.join(", ")}`);
} else if (isUser(value)) {
// TypeScript 知道这里是 User
console.log(`用户: ${value.name}`);
console.log(`邮箱: ${value.email}`);
} else {
console.log("无效的用户数据");
}
}处理联合类型的类型守卫
typescript
// 定义联合类型
type Response =
| { status: "success"; data: User }
| { status: "error"; message: string };
interface User {
id: number;
name: string;
}
// 类型守卫:检查是否为成功响应
function isSuccessResponse(response: Response): response is { status: "success"; data: User } {
return response.status === "success";
}
// 类型守卫:检查是否为错误响应
function isErrorResponse(response: Response): response is { status: "error"; message: string } {
return response.status === "error";
}
// 使用类型守卫处理响应
function handleResponse(response: Response): void {
if (isSuccessResponse(response)) {
// TypeScript 知道这里是成功响应
console.log(`用户: ${response.data.name}`);
console.log(`ID: ${response.data.id}`);
} else if (isErrorResponse(response)) {
// TypeScript 知道这里是错误响应
console.error(`错误: ${response.message}`);
}
}
// 使用示例
const successResponse: Response = {
status: "success",
data: { id: 1, name: "John" }
};
const errorResponse: Response = {
status: "error",
message: "用户不存在"
};
handleResponse(successResponse); // 输出: 用户: John, ID: 1
handleResponse(errorResponse); // 输出: 错误: 用户不存在使用示例
示例 1:处理 API 响应
typescript
// 定义 API 响应类型
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string };
interface User {
id: number;
name: string;
email: string;
}
// 类型守卫:检查是否为成功响应
function isSuccessResponse<T>(response: ApiResponse<T>): response is { success: true; data: T } {
return response.success === true;
}
// 处理 API 响应
async function fetchUser(id: number): Promise<ApiResponse<User>> {
// 模拟 API 调用
return {
success: true,
data: {
id: 1,
name: "John Doe",
email: "john@example.com"
}
};
}
// 使用类型守卫安全处理响应
async function getUser(id: number): Promise<User | null> {
const response = await fetchUser(id);
if (isSuccessResponse(response)) {
// TypeScript 知道这里是成功响应
return response.data; // ✅ 类型安全
} else {
// TypeScript 知道这里是错误响应
console.error(response.error);
return null;
}
}示例 2:处理表单数据
typescript
// 定义表单数据类型
type FormData =
| { type: "text"; value: string }
| { type: "number"; value: number }
| { type: "checkbox"; value: boolean };
// 类型守卫:检查是否为文本类型
function isTextFormData(data: FormData): data is { type: "text"; value: string } {
return data.type === "text";
}
// 类型守卫:检查是否为数字类型
function isNumberFormData(data: FormData): data is { type: "number"; value: number } {
return data.type === "number";
}
// 处理表单数据
function processFormData(data: FormData): void {
if (isTextFormData(data)) {
// TypeScript 知道这里是文本类型
console.log(`文本: ${data.value.toUpperCase()}`);
} else if (isNumberFormData(data)) {
// TypeScript 知道这里是数字类型
console.log(`数字: ${data.value.toFixed(2)}`);
} else {
// TypeScript 知道这里是复选框类型
console.log(`复选框: ${data.value ? "选中" : "未选中"}`);
}
}示例 3:处理 DOM 元素
typescript
// 类型守卫:检查是否为 HTMLInputElement
function isInputElement(element: HTMLElement | null): element is HTMLInputElement {
return element !== null && element instanceof HTMLInputElement;
}
// 类型守卫:检查是否为 HTMLButtonElement
function isButtonElement(element: HTMLElement | null): element is HTMLButtonElement {
return element !== null && element instanceof HTMLButtonElement;
}
// 处理 DOM 元素
function handleElement(element: HTMLElement | null): void {
if (isInputElement(element)) {
// TypeScript 知道这里是 HTMLInputElement
console.log(`输入值: ${element.value}`);
console.log(`类型: ${element.type}`);
} else if (isButtonElement(element)) {
// TypeScript 知道这里是 HTMLButtonElement
console.log(`按钮文本: ${element.textContent}`);
console.log(`是否禁用: ${element.disabled}`);
} else if (element === null) {
console.log("元素不存在");
} else {
console.log("其他类型的元素");
}
}示例 4:处理 JSON 数据
typescript
// 定义数据结构
interface Config {
apiUrl: string;
timeout: number;
retries: number;
}
// 类型守卫:检查是否为有效的配置对象
function isConfig(value: unknown): value is Config {
return (
typeof value === "object" &&
value !== null &&
"apiUrl" in value &&
"timeout" in value &&
"retries" in value &&
typeof (value as any).apiUrl === "string" &&
typeof (value as any).timeout === "number" &&
typeof (value as any).retries === "number"
);
}
// 安全解析 JSON
function parseConfig(jsonString: string): Config | null {
try {
const parsed = JSON.parse(jsonString);
if (isConfig(parsed)) {
// TypeScript 知道这里是 Config
return parsed; // ✅ 类型安全
}
return null;
} catch {
return null;
}
}
// 使用示例
const configJson = `{
"apiUrl": "https://api.example.com",
"timeout": 5000,
"retries": 3
}`;
const config = parseConfig(configJson);
if (config) {
// TypeScript 知道 config 是 Config 类型
console.log(config.apiUrl); // ✅ 可以访问
console.log(config.timeout); // ✅ 可以访问
console.log(config.retries); // ✅ 可以访问
}类型守卫的组合使用
可以组合使用多种类型守卫来实现更复杂的类型检查:
typescript
// 定义类型
type Value = string | number | boolean | null | undefined;
// 组合使用多种类型守卫
function processValue(value: Value): void {
// 先检查 null 和 undefined
if (value == null) {
console.log("值为 null 或 undefined");
return;
}
// 然后使用 typeof 检查类型
if (typeof value === "string") {
console.log(`字符串: ${value.toUpperCase()}`);
} else if (typeof value === "number") {
console.log(`数字: ${value.toFixed(2)}`);
} else if (typeof value === "boolean") {
console.log(`布尔值: ${value ? "真" : "假"}`);
}
}
// 组合自定义类型守卫
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
interface Cat extends Animal {
color: string;
meow(): void;
}
// 类型守卫:检查是否为 Animal
function isAnimal(value: unknown): value is Animal {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
typeof (value as any).name === "string"
);
}
// 类型守卫:检查是否为 Dog
function isDog(value: unknown): value is Dog {
if (!isAnimal(value)) {
return false;
}
return (
"breed" in value &&
"bark" in value &&
typeof (value as any).breed === "string" &&
typeof (value as any).bark === "function"
);
}
// 使用组合的类型守卫
function handleAnimal(value: unknown): void {
if (!isAnimal(value)) {
console.log("不是动物");
return;
}
// TypeScript 知道这里是 Animal
console.log(`动物名称: ${value.name}`);
if (isDog(value)) {
// TypeScript 知道这里是 Dog
value.bark();
console.log(`品种: ${value.breed}`);
} else {
// TypeScript 知道这里是其他动物(可能是 Cat)
console.log("不是狗");
}
}类型守卫 vs 类型断言
类型守卫和类型断言是处理类型不确定性的两种不同方式:
typescript
// 类型断言:告诉编译器"相信我,这就是这个类型"(不进行运行时检查)
function processWithAssertion(value: string | number): void {
const str = value as string; // 断言为 string
console.log(str.toUpperCase()); // 如果 value 是 number,运行时错误
}
// 类型守卫:运行时检查,更安全
function processWithGuard(value: string | number): void {
if (typeof value === "string") {
// TypeScript 自动推断为 string
console.log(value.toUpperCase()); // ✅ 安全
} else {
// TypeScript 自动推断为 number
console.log(value.toFixed(2)); // ✅ 安全
}
}提示
优先使用类型守卫而不是类型断言。类型守卫在运行时进行实际检查,更安全可靠。类型断言应该只在确实确定类型且无法使用类型守卫时使用。
常见错误和解决方案
错误示例
typescript
// ❌ 错误:类型守卫函数没有返回类型谓词
function isStringWrong(value: unknown): boolean {
return typeof value === "string";
// 这样 TypeScript 不会进行类型缩小
}
function processWrong(value: unknown): void {
if (isStringWrong(value)) {
// TypeScript 仍然不知道 value 是 string
console.log(value.toUpperCase()); // ❌ 类型错误
}
}
// ❌ 错误:使用类型断言代替类型守卫
function processUnsafe(value: string | number): void {
const str = value as string; // 不安全的断言
console.log(str.toUpperCase()); // 如果 value 是 number,运行时错误
}
// ❌ 错误:typeof null 返回 "object"
function processNullable(value: string | null): void {
if (typeof value === "object") {
// TypeScript 认为这里是 null,但 typeof null 也是 "object"
console.log(value); // value 是 null
}
}正确写法
typescript
// ✅ 正确:使用类型谓词
function isString(value: unknown): value is string {
return typeof value === "string";
}
function process(value: unknown): void {
if (isString(value)) {
// TypeScript 知道这里是 string
console.log(value.toUpperCase()); // ✅ 类型安全
}
}
// ✅ 正确:使用类型守卫
function processSafe(value: string | number): void {
if (typeof value === "string") {
// TypeScript 自动推断为 string
console.log(value.toUpperCase()); // ✅ 安全
} else {
// TypeScript 自动推断为 number
console.log(value.toFixed(2)); // ✅ 安全
}
}
// ✅ 正确:检查 null
function processNullable(value: string | null): void {
if (value === null) {
console.log("值为 null");
} else {
// TypeScript 知道这里是 string
console.log(value.toUpperCase()); // ✅ 安全
}
}注意事项
提示
- 类型守卫通过运行时检查来缩小类型范围,比类型断言更安全
typeof用于检查基本类型(string、number、boolean 等)instanceof用于检查对象是否是某个类的实例in操作符用于检查对象是否包含某个属性- 自定义类型守卫必须返回类型谓词(
parameterName is Type) - 类型守卫可以组合使用,实现更复杂的类型检查
注意
typeof null返回"object":这是 JavaScript 的历史遗留问题。要检查null,应该直接使用value === null或value !== null- 类型守卫必须返回类型谓词:自定义类型守卫函数必须返回
parameterName is Type格式,否则 TypeScript 不会进行类型缩小 - 类型守卫是运行时检查:类型守卫在运行时进行实际检查,如果检查逻辑错误,可能导致类型推断错误
- 避免过度使用类型断言:如果频繁使用类型断言,应该考虑使用类型守卫来替代
信息
类型守卫在以下场景特别有用:
- 处理联合类型,需要根据运行时值来确定具体类型
- 处理
unknown类型,需要安全地缩小类型范围 - 处理 API 响应,需要检查响应结构
- 处理 DOM 元素,需要确定具体的元素类型
- 处理 JSON 数据,需要验证数据结构
- 处理第三方库数据,需要确保类型安全
重要
- 类型守卫是运行时检查:类型守卫在运行时进行实际检查,确保类型安全。这与类型断言不同,类型断言只是告诉编译器如何理解类型,不进行运行时验证
- 类型谓词是必需的:自定义类型守卫函数必须返回类型谓词(
parameterName is Type),否则 TypeScript 不会进行类型缩小 - 检查逻辑必须正确:类型守卫的检查逻辑必须准确反映类型关系,否则可能导致类型推断错误和运行时错误
- 优先使用类型守卫:在处理类型不确定性时,优先使用类型守卫而不是类型断言,类型守卫更安全可靠
相关链接
- 基础类型 - 了解 TypeScript 的基础类型
- 联合类型 - 学习联合类型,类型守卫常用于处理联合类型
- 类型断言 - 了解类型断言,对比类型守卫和类型断言的区别
- 类型推断 - 了解类型推断机制,类型守卫是类型缩小的重要方式
- TypeScript 官方文档 - 类型守卫