TypeScript 类型保护(Type Guards)

60 阅读5分钟

在 TypeScript 的开发过程中,类型安全是我们选择 TypeScript 的主要原因之一。但当我们在处理联合类型、复杂对象或外部 API 数据时,经常会遇到类型不确定的情况。这时,类型保护(Type Guards) 就成了我们处理类型不确定性的强大武器。

什么是类型保护?

类型保护是一种特殊的表达式或函数,它们在运行时检查类型,并在条件块中为 TypeScript 编译提供额外的类型信息。简单来说,它告诉 TypeScript:"在这个代码块内,我确定这个变量的类型是什么"。

为什么需要类型保护?

在 TypeScript 中,当你声明一个变量为联合类型时:

let value: string | number;

TypeScript 知道 value 可能是字符串或数字,但当你尝试使用特定类型的方法时:

value.toUpperCase(); // 错误:类型"number"上不存在属性"toUpperCase"

这时就需要类型保护来区分具体类型。

内置类型保护机制

1. typeof 类型保护

处理基本类型时最常用的保护:

function padLeft(value: string | number, padding: number) {
    if (typeof value === 'string') {
        // 在此块内,TypeScript 知道 value 是 string
        return value.padStart(padding);
    }
    // 在此块内,TypeScript 知道 value 是 number
    return String(value).padStart(padding);
}

支持的类型:"string", "number", "bigint", "boolean", "symbol", "undefined", "object", "function"

注意typeof null === "object",所以不能用于区分 null

2. instanceof 类型保护

用于检查类的实例类型:

class ApiError extends Error {
    constructor(message: string, public status: number) {
        super(message);
    }
}

function handleError(error: Error) {
    if (error instanceof ApiError) {
        // 在此块内,TypeScript 知道 error 是 ApiError
        console.error(`API错误 [${error.status}]: ${error.message}`);
        retryRequest();
    } else {
        // 其他类型的错误
        logGenericError(error);
    }
}

3. in 操作符类型保护

检查对象是否包含特定属性,特别适用于区分联合类型中的不同对象类型:

interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    layEggs(): void;
}

function move(pet: Bird | Fish) {
    if ('fly' in pet) {
        // 在此块内,TypeScript 知道 pet 是 Bird
        pet.fly();
    } else {
        // 在此块内,TypeScript 知道 pet 是 Fish
        pet.swim();
    }
}

自定义类型保护函数

当内置类型保护不能满足需求时,你可以创建自定义类型保护函数。它们是一种返回类型谓词的特殊函数:

function isApiResponse(data: any): data is ApiResponse {
    return data && 
           typeof data.status === 'number' &&
           Array.isArray(data.items);
}

interface ApiResponse {
    status: number;
    items: any[];
}

function handleResponse(data: unknown) {
    if (isApiResponse(data)) {
        // 在此块内,TypeScript 知道 data 是 ApiResponse
        console.log(`状态:${data.status},项目数:${data.items.length}`);
    } else {
        console.error('无效的API响应');
    }
}

实际案例:表单验证

interface UserForm {
    name: string;
    email: string;
    age: number;
}

function isValidUserForm(data: any): data is UserForm {
    return typeof data === 'object' &&
        typeof data.name === 'string' && data.name.trim() !== '' &&
        typeof data.email === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email) &&
        typeof data.age === 'number' && data.age > 0;
}

function processForm(formData: unknown) {
    if (isValidUserForm(formData)) {
        // 安全地访问 UserForm 的属性
        registerUser(formData);
    } else {
        // 表单校验失败处理
        showValidationErrors();
    }
}

基于字面量的类型保护

当联合类型包含字面量时(如字符串字面量或数字字面量),可以使用相等性检查进行类型保护:

type ButtonSize = 'small' | 'medium' | 'large';

function getPadding(size: ButtonSize) {
    if (size === 'small') {
        return 4;
    } else if (size === 'medium') {
        return 8;
    } else {
        // 这里 size 只能是 'large'
        return 16;
    }
}

高级类型保护技术

1. 用户定义的类型保护与 never

结合 never 类型进行穷尽性检查:

type Shape = 'circle' | 'square' | 'triangle';

function getArea(shape: Shape, size: number): number {
    switch(shape) {
        case 'circle':
            return Math.PI * size ** 2;
        case 'square':
            return size ** 2;
        case 'triangle':
            return (Math.sqrt(3) / 4) * size ** 2;
        default:
            // 确保处理了所有情况
            const _exhaustiveCheck: never = shape;
            return _exhaustiveCheck;
    }
}

2. 过滤数组中的特定类型

function isStringArray(value: any): value is string[] {
    return Array.isArray(value) && value.every(item => typeof item === 'string');
}

function processInput(input: string | string[]) {
    if (isStringArray(input)) {
        // 处理字符串数组
        return input.map(str => str.toUpperCase()).join(', ');
    } else {
        // 处理单个字符串
        return input.toUpperCase();
    }
}

3. 可区分联合(Discriminated Unions)

这是一种使用公共字段区分联合类型的方法:

interface SuccessResponse {
    type: 'success';
    data: any;
}

interface ErrorResponse {
    type: 'error';
    code: number;
    message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
    switch (response.type) {
        case 'success':
            // TypeScript 知道这里的 response 是 SuccessResponse
            console.log('成功获取数据:', response.data);
            break;
        case 'error':
            // TypeScript 知道这里的 response 是 ErrorResponse
            console.error(`错误 ${response.code}: ${response.message}`);
            break;
    }
}

真实应用场景

1. 处理 API 响应

type User = {
    id: number;
    name: string;
    email: string;
};

type ApiResult<T> = 
    | { status: 'loading' }
    | { status: 'success', data: T }
    | { status: 'error', message: string };

function UserProfile({ result }: { result: ApiResult<User> }) {
    if (result.status === 'loading') {
        return <Spinner />;
    }
    
    if (result.status === 'error') {
        return <ErrorMessage message={result.message} />;
    }
    
    // TypeScript 知道这里是 'success' 状态
    return (
        <div>
            <h1>{result.data.name}</h1>
            <p>Email: {result.data.email}</p>
        </div>
    );
}

2. 表单状态管理

type FormState =
    | { state: 'idle' }
    | { state: 'editing', values: FormValues, errors: FormErrors }
    | { state: 'submitting' }
    | { state: 'submitted' }
    | { state: 'error', message: string };

function formReducer(state: FormState, action: FormAction): FormState {
    // 使用类型保护处理不同状态
}

function submitForm(state: FormState) {
    if (state.state !== 'editing') {
        // 非编辑状态不允许提交
        return;
    }
    
    // 在这里,TypeScript 知道 state 是 editing 状态
    const { values } = state;
    // 提交表单逻辑...
}

3. Redux 状态管理

在 Redux 的 reducer 中,类型保护特别有用:

type Action = 
    | { type: 'ADD_TODO', text: string }
    | { type: 'TOGGLE_TODO', id: number }
    | { type: 'REMOVE_TODO', id: number };

function todosReducer(state: Todo[] = [], action: Action): Todo[] {
    switch (action.type) {
        case 'ADD_TODO':
            // TypeScript 知道 action 有 text 属性
            return [...state, { id: Date.now(), text: action.text, completed: false }];
        case 'TOGGLE_TODO':
            // TypeScript 知道 action 有 id 属性
            return state.map(todo => 
                todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
            );
        case 'REMOVE_TODO':
            return state.filter(todo => todo.id !== action.id);
        default:
            // 安全处理
            const exhaustiveCheck: never = action;
            return state;
    }
}

类型保护的最佳实践

  1. 优先使用内置类型保护typeofinstanceofin 操作符是最常用且最高效的类型保护方式

  2. 创建可重用的类型保护函数:对于复杂对象验证,创建自定义类型保护函数并在项目中复用

  3. 使用可区分联合模式:为复杂联合类型添加"标签"字段(如 typekindstatus),简化类型保护逻辑

  4. 结合错误处理:在类型保护中增加适当的错误处理或日志记录,便于调试

  5. 避免过度复杂的保护函数:保持类型保护函数简单和高效,避免嵌套太深的条件判断

  6. 为自定义类型保护编写测试:确保类型保护函数在各种边界条件下都能正确工作

常见陷阱与解决方案

陷阱 1: 误用类型保护返回值

错误示例

function isString(value: any): boolean {
    return typeof value === 'string';
}

function example(value: string | number) {
    if (isString(value)) {
        value.toUpperCase(); // 错误:'value' 仍被视为 string | number
    }
}

解决方案:使用类型谓词

function isString(value: any): value is string {
    return typeof value === 'string';
}

陷阱 2: 过度复杂的保护条件

错误示例

function isUser(obj: any): obj is User {
    return obj && 
        typeof obj.id === 'number' &&
        typeof obj.name === 'string' &&
        typeof obj.email === 'string' &&
        // ...20多个属性检查...
}

解决方案:使用库如 io-tszod 进行复杂的运行时验证:

import * as t from 'io-ts';

const User = t.type({
    id: t.number,
    name: t.string,
    email: t.string,
    // ...
});

type User = t.TypeOf<typeof User>;

function validateUser(input: unknown): User | null {
    const result = User.decode(input);
    return result._tag === 'Right' ? result.right : null;
}

陷阱 3: 忽略 null/undefined 检查

错误示例

function getLength(value: string | null) {
    if (typeof value === 'string') {
        return value.length;
    }
    return 0;
}

解决方案:明确检查 null/undefined

function getLength(value: string | null) {
    if (value != null) { // 同时检查 null 和 undefined
        return value.length;
    }
    return 0;
}

类型安全的关键工具

TypeScript 的类型保护是处理类型不确定性的关键工具,它允许我们:

  • 🛡️ 在不确定的上下文中安全地使用特定类型的方法和属性
  • 🔍 清晰地区分联合类型的不同成员
  • 🎯 在条件分支中提供精确的类型推断
  • 📝 创建可重用、可维护的类型验证逻辑
  • 🚀 提高代码的健壮性和可读性

掌握类型保护不仅能消除 TypeScript 中的类型错误,还能帮助我们在设计复杂类型系统时保持清晰。在 TypeScript 的世界里,类型保护是我们连接运行时与编译时类型信息的重要桥梁

"优秀的程序员不只是让代码工作,而是让代码以最明确、最安全的方式工作。类型保护是实现这一目标的重要工具。" —— TypeScript 进阶之道