文章目录

TypeScript 联合类型:string | number 与类型保护

发布于 2026-04-04 22:59:03 · 浏览 16 次 · 评论 0 条

TypeScript 联合类型:string | number 与类型保护

联合类型是 TypeScript 类型系统中极为实用的特性,它允许一个值在多种类型之间灵活切换。然而,有利必有弊——TypeScript 在处理联合类型时,只能访问所有类型共有的属性和方法。这时,类型保护机制应运而生,它让 TypeScript 能够在特定代码块中精确识别变量的具体类型。


1. 联合类型的基本概念

联合类型使用竖线 | 分隔多个类型,表示一个值可以是这些类型中的任意一种。想象一个场景:某个函数的参数既可能是字符串,也可能是数字。

function formatValue(value: string | number): string {
    return String(value);
}

在这个例子中,value 的类型是 string | number,TypeScript 允许我们对这个变量调用所有类型共有的方法。String(value) 能够正常工作,因为无论 value 是字符串还是数字,都能转换为字符串。

再看一个更实际的例子。假设你正在开发一个配置文件解析器,某个配置项既可以是字符串类型,也可以是数组类型。

type ConfigValue = string | string[];

function processConfig(value: ConfigValue): void {
    if (Array.isArray(value)) {
        console.log(`收到数组,共 ${value.length} 个元素`);
    } else {
        console.log(`收到字符串:${value}`);
    }
}

这段代码展示了联合类型的核心思想:同一个变量在不同场景下扮演不同角色。TypeScript 的类型系统会在背后默默跟踪这些可能的类型,并在代码执行过程中逐步缩小类型的范围。


2. 类型保护的必要性

当一个变量的类型是联合类型时,TypeScript 只能让我们访问所有类型共有的成员。一旦需要使用某个类型特有的属性或方法,编译器就会报错。

考虑以下场景:你有一个 User 类型,可能是普通用户,也可能是管理员。

interface BasicUser {
    type: 'basic';
    username: string;
    email: string;
}

interface AdminUser {
    type: 'admin';
    username: string;
    email: string;
    permissions: string[];
}

type User = BasicUser | AdminUser;

如果你尝试直接访问 permissions 属性,TypeScript 会明确告诉你这个属性在 BasicUser 类型中不存在。

function printUserInfo(user: User): void {
    // 错误:Property 'permissions' does not exist on type 'User'
    console.log(user.permissions);
}

这就是类型保护发挥作用的地方。通过运行时检查,TypeScript 能够推断出在特定代码分支中变量的具体类型,从而允许访问该类型特有的属性。


3. typeof 类型守卫

typeof 是 JavaScript 原生的类型检查运算符,TypeScript 对其有专门的支持。当你在条件语句中使用 typeof 时,TypeScript 会自动缩小联合类型的范围。

function processInput(input: string | number): void {
    if (typeof input === 'string') {
        // 在这个分支中,input 被识别为 string 类型
        console.log(`字符串长度:${input.length}`);
        console.log(input.toUpperCase());
    } else {
        // 在 else 分支中,input 被识别为 number 类型
        console.log(`数字值:${input.toFixed(2)}`);
        console.log(Math.round(input));
    }
}

typeof 类型守卫支持以下比较操作符:"string""number""bigint""boolean""symbol""undefined""object""function"。需要特别注意的是,typeof null 返回 "object",所以在使用 typeof 进行类型保护时要额外注意空值的情况。

function handleValue(value: string | null): void {
    if (typeof value === 'string') {
        console.log(`字符串内容:${value}`);
    }
    // 注意:typeof 无法区分 null 和 object
    // 这里需要额外的 null 检查
}
```

---

## 4. 自定义类型守卫:类型谓词

当 `typeof` 无法满足需求时,可以定义自定义的类型守卫。这种方式通过返回类型谓词(type predicate)来告诉 TypeScript "这个变量在特定条件下就是某种类型"。

类型谓词的语法是 `parameterName is Type`,它是一个布尔函数,但其返回值会让 TypeScript 在调用处进行类型收窄。

假设你有一个数据解析场景,后端返回的数据可能是有效的 `User` 对象,也可能是解析失败的 `null`。

```typescript
interface User {
    id: number;
    name: string;
    age: number;
}

interface RawData {
    _tag: 'valid';
    data: User;
}

interface InvalidData {
    _tag: 'invalid';
    error: string;
}

type ParsedResult = RawData | InvalidData;

function isValidUser(data: ParsedResult): data is RawData {
    return data._tag === 'valid';
}

function handleParsedResult(result: ParsedResult): void {
    if (isValidUser(result)) {
        // 在这里,result 被识别为 RawData 类型
        console.log(`有效用户:${result.data.name}`);
        console.log(`用户ID:${result.data.id}`);
    } else {
        console.log(`解析失败:${result.error}`);
    }
}

再看一个更复杂的例子。假设你需要从混合数组中筛选出特定类型的元素。

interface Dog {
    species: 'dog';
    name: string;
    bark(): void;
}

interface Cat {
    species: 'cat';
    name: string;
    meow(): void;
}

interface Bird {
    species: 'bird';
    name: string;
    fly(): void;
}

type Pet = Dog | Cat | Bird;

function isDog(pet: Pet): pet is Dog {
    return pet.species === 'dog';
}

function isCat(pet: Pet): pet is Cat {
    return pet.species === 'cat';
}

function processPets(pets: Pet[]): void {
    pets.forEach(pet => {
        if (isDog(pet)) {
            pet.bark();
        } else if (isCat(pet)) {
            pet.meow();
        } else {
            pet.fly();
        }
    });
}

5. in 操作符类型守卫

当对象类型包含可辨识的属性时,in 操作符可以用于类型保护。这种方式特别适合处理具有"标签"字段的对象。

interface Circle {
    kind: 'circle';
    radius: number;
}

interface Square {
    kind: 'square';
    sideLength: number;
}

interface Rectangle {
    kind: 'rectangle';
    width: number;
    height: number;
}

type Shape = Circle | Square | Rectangle;

function getArea(shape: Shape): number {
    if ('radius' in shape) {
        // shape 被识别为 Circle
        return Math.PI * shape.radius ** 2;
    }

    if ('sideLength' in shape) {
        // shape 被识别为 Square
        return shape.sideLength ** 2;
    }

    // shape 被识别为 Rectangle
    return shape.width * shape.height;
}

in 操作符在处理第三方库返回的类型时特别有用,因为这些类型可能没有明确的类型谓词定义。


6. instanceof 类型守卫

instanceof 检查对象的原型链,是另一种强大的类型保护机制。它适用于检查对象是否是某个类的实例。

class ApiResponse {
    status: number;
    data: unknown;

    constructor(status: number, data: unknown) {
        this.status = status;
        this.data = data;
    }
}

class ErrorResponse {
    status: number;
    message: string;

    constructor(status: number, message: string) {
        this.status = status;
        this.message = message;
    }
}

type Response = ApiResponse | ErrorResponse;

function handleResponse(response: Response): void {
    if (response instanceof ErrorResponse) {
        console.log(`错误响应:${response.message}`);
    } else {
        console.log(`API 响应状态:${response.status}`);
        console.log(`数据:${JSON.stringify(response.data)}`);
    }
}
```

`instanceof` 的优势在于它检查的是实际的原型关系,这使得它在处理类层次结构时特别可靠。

---

## 7. 可辨识联合与穷举检查

可辨识联合(Discriminated Union)是一种将类型保护与模式匹配结合的高级技巧。通过一个公共的可辨识属性(通常是字符串字面量),可以清晰地区分不同的类型变体。

```typescript
interface LoadingState {
    status: 'loading';
    progress: number;
}

interface SuccessState {
    status: 'success';
    data: string[];
}

interface ErrorState {
    status: 'error';
    message: string;
}

type AsyncState = LoadingState | SuccessState | ErrorState;

function renderState(state: AsyncState): string {
    switch (state.status) {
        case 'loading':
            return `加载中:${state.progress}%`;
        case 'success':
            return `成功加载 ${state.data.length} 条记录`;
        case 'error':
            return `错误:${state.message}`;
        default:
            // 穷举检查:确保所有状态都被处理
            const exhaustiveCheck: never = state;
            return exhaustiveCheck;
    }
}

这里的关键是 default 分支中的 never 类型赋值。如果后续添加了新的状态类型但忘记在 switch 中处理,TypeScript 会在编译时抛出错误,强制你完善所有分支。


8. 实际应用场景

在实际项目中,类型保护广泛用于数据处理、外部接口调用和表单验证等场景。

场景一:表单输入处理

interface TextInput {
    type: 'text';
    value: string;
    placeholder: string;
}

interface NumberInput {
    type: 'number';
    value: number;
    min: number;
    max: number;
}

type InputField = TextInput | NumberInput;

function getInputValue(field: InputField): string {
    if (field.type === 'text') {
        return field.value;
    } else {
        return String(field.value);
    }
}

function validateField(field: InputField): boolean {
    if (field.type === 'number') {
        return field.value >= field.min && field.value <= field.max;
    }
    return field.value.length > 0;
}

场景二:API 响应处理

interface ApiSuccess<T> {
    success: true;
    data: T;
    meta?: {
        page: number;
        total: number;
    };
}

interface ApiError {
    success: false;
    error: {
        code: number;
        message: string;
    };
}

type ApiResponse<T> = ApiSuccess<T> | ApiError;

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
    const response = await fetch(url);
    const data = await response.json();
    return data;
}

async function useApiData<T>(url: string): Promise<T | null> {
    const result = await fetchData<T>(url);

    if (!result.success) {
        console.error(`API 错误:${result.error.message}`);
        return null;
    }
    
    return result.data;
}
```

**场景三:配置解析**

```typescript
interface ColorConfig {
    mode: 'color';
    hex: string;
    alpha?: number;
}

interface PatternConfig {
    mode: 'pattern';
    url: string;
    repeat: 'repeat' | 'no-repeat';
}

type StyleConfig = ColorConfig | PatternConfig;

function parseStyle(config: StyleConfig): string {
    if (config.mode === 'color') {
        const alpha = config.alpha ?? 1;
        return `rgba(...)`; // 颜色处理逻辑
    }
    return `url(${config.url}) ${config.repeat}`;
}

9. 最佳实践总结

在项目中使用联合类型和类型保护时,遵循以下原则能让代码更加健壮和可维护:

场景 推荐方案
基础类型判断 typeof 类型守卫
类实例判断 instanceof 类型守卫
带有可辨识属性的对象 可辨识联合 + switch
复杂类型判断 自定义类型谓词
确保所有分支被处理 never 类型穷举检查

类型保护的核心价值在于它让 TypeScript 的类型系统在运行时也能发挥作用。通过合理的类型保护设计,你既能享受静态类型检查的安全性,又能在具体业务逻辑中获得精确的类型提示。这种能力在大型项目中尤为重要——它能帮助你及早发现潜在的 bug,同时大幅提升开发效率。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文