文章目录

TypeScript 类型守卫:typeof 与 instanceof

发布于 2026-04-06 01:59:45 · 浏览 13 次 · 评论 0 条

TypeScript 类型守卫:typeof 与 instanceof

在 TypeScript 的类型系统中,类型守卫是让代码在运行时精准判断变量类型的机制。它解决的问题是:TypeScript 的类型推断是静态的,但实际运行时数据类型是动态的。当我们从外部获取数据(比如用户输入、API 返回)时,变量的类型在编译阶段往往是不确定的。这时候就需要类型守卫来帮助 TypeScript 理解"这个变量在特定条件下就是某种类型"。

typeofinstanceof 是最常用的两种类型守卫,它们分别适用于不同的场景。理解它们的区别和用法,是写出健壮 TypeScript 代码的基础。


一、typeof 类型守卫:处理原始类型

typeof 运算符在 TypeScript 中可以精确判断原始类型(string、number、boolean、symbol、bigint、undefined)。它的返回值是字符串,可以在条件语句中直接使用。

function processValue(value: string | number) {
    if (typeof value === "string") {
        // TypeScript 知道 value 在这里一定是 string
        console.log(`字符串长度为: ${value.length}`);
    } else {
        // TypeScript 推断 value 是 number
        console.log(`数值的绝对值是: ${Math.abs(value)}`);
    }
}

上面的例子中,typeof value === "string" 这个条件不仅在运行时判断了类型,还向 TypeScript 类型系统传递了信息:在 if 分支内,value 被收窄为 string 类型;在 else 分支内,自动收窄为 number

typeof 能判断的类型

类型 typeof 返回值
string "string"
number "number"
boolean "boolean"
symbol "symbol"
bigint "bigint"
undefined "undefined"
function "function"
数组、对象、null "object"

需要注意两点:typeof null 返回 "object",这是 JavaScript 的历史遗留问题;另外,所有对象类型(包括数组)在 typeof 下都返回 "object"。因此,typeof 只适合处理原始类型,不适合判断对象的具体结构。

实际应用场景

假设你需要处理一个配置对象,其中某些字段可能是字符串也可能是数字:

interface Config {
    timeout: string | number;
    retries: string | number;
}

function validateConfig(config: Config) {
    if (typeof config.timeout === "string") {
        config.timeout = parseInt(config.timeout, 10);
    }
    if (typeof config.retries === "string") {
        config.retries = parseInt(config.retries, 10);
    }
}

通过类型守卫,我们确保在后续代码中使用 config.timeoutconfig.retries 时,它们已经是 number 类型,可以直接参与数值计算。


二、instanceof 类型守卫:处理对象实例

instanceof 运算符用于判断某个对象是否是某个类的实例。与 typeof 不同,instanceof 可以识别对象的具体类型,包括自定义类、日期对象、错误对象等。

class ApiError extends Error {
    statusCode: number;

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

class ValidationError extends Error {
    field: string;

    constructor(message: string, field: string) {
        super(message);
        this.field = field;
    }
}

function handleError(error: Error) {
    if (error instanceof ApiError) {
        // TypeScript 知道 error 是 ApiError 类型
        console.log(`API 错误,状态码: ${error.statusCode}`);
    } else if (error instanceof ValidationError) {
        // TypeScript 知道 error 是 ValidationError 类型
        console.log(`验证错误,字段: ${error.field}`);
    } else {
        // 通用 Error 处理
        console.log(`未知错误: ${error.message}`);
    }
}
```

`instanceof` 的核心优势在于它能识别对象的原型链。如果 `ApiError` 继承了 `Error`,那么 `error instanceof ApiError` 会正确返回 `true`,同时 TypeScript 会将 `error` 的类型收窄为 `ApiError`。

### 常见对象的 instanceof 判断

```typescript
const dates = new Date();
const arr = [1, 2, 3];
const obj = { a: 1 };
const err = new Error("出错了");

console.log(dates instanceof Date);      // true
console.log(arr instanceof Array);       // true
console.log(obj instanceof Object);      // true
console.log(err instanceof Error);       // true
```

与 `typeof` 不同,`instanceof` 可以准确区分数组和普通对象。当需要处理数组时,`instanceof Array` 是更可靠的选择。

### 自定义类与接口的实现

需要注意的是,`instanceof` 只能判断通过 `class` 创建的实例,不能直接用于判断接口实现。因为接口是 TypeScript 的静态概念,在编译后会被完全擦除,而 `instanceof` 依赖的是运行时的原型链。

```typescript
interface Loggable {
    log(): void;
}

class ConsoleLogger implements Loggable {
    log() {
        console.log("控制台日志");
    }
}

class FileLogger implements Loggable {
    log() {
        console.log("文件日志");
    }
}

function writeLog(logger: Loggable) {
    // ❌ 错误:接口无法在运行时检测
    // if (logger instanceof Loggable) { }
    
    // ✅ 正确:检查实例的具体类型
    if (logger instanceof ConsoleLogger) {
        logger.log();
    } else if (logger instanceof FileLogger) {
        logger.log();
    }
}
```

---

## 三、typeof 与 instanceof 的核心区别

`typeof` 和 `instanceof` 的选择取决于你要判断的数据类型。原始类型使用 `typeof`,对象实例使用 `instanceof`。这个选择直接影响代码的类型安全性和可读性。

```typescript
function identifyType(value: unknown) {
    if (typeof value === "string") {
        console.log("这是一个字符串");
    } else if (typeof value === "number") {
        console.log("这是一个数字");
    } else if (value instanceof Date) {
        console.log("这是一个 Date 对象");
    } else if (value instanceof Array) {
        console.log("这是一个数组");
    } else {
        console.log("未知类型");
    }
}
```

上面这个示例展示了两种类型守卫的典型用法。`typeof` 处理原始类型(string、number 等),`instanceof` 处理复杂对象(Date、Array 等)。这种组合使用的方式在实际开发中非常常见。

---

## 四、进阶技巧:自定义类型守卫

除了 `typeof` 和 `instanceof`,TypeScript 还支持自定义类型守卫函数。这种机制允许开发者定义任意的类型判断逻辑,并返回类型谓词(type predicate)告诉 TypeScript"这个条件成立时,变量是什么类型"。

### 数组类型守卫

`Array.isArray()` 是 JavaScript 内置的方法,用于判断某个值是否是数组。在 TypeScript 中使用时,配合类型守卫可以正确收窄类型:

```typescript
function processItems(items: string | string[]) {
    if (Array.isArray(items)) {
        // TypeScript 知道 items 是 string[]
        console.log(`收到数组,共 ${items.length} 个元素`);
        items.forEach(item => console.log(item));
    } else {
        // TypeScript 知道 items 是 string
        console.log(`收到单个字符串: ${items}`);
    }
}
```

### 自定义类型谓词函数

当内置方法无法满足需求时,可以定义自己的类型守卫函数:

```typescript
interface Dog {
    bark(): void;
    breed: string;
}

interface Cat {
    meow(): void;
    color: string;
}

type Pet = Dog | Cat;

function isDog(pet: Pet): pet is Dog {
    return (pet as Dog).bark !== undefined;
}

function isCat(pet: Pet): pet is Cat {
    return (pet as Cat).meow !== undefined;
}

function handlePet(pet: Pet) {
    if (isDog(pet)) {
        // pet 被收窄为 Dog
        pet.bark();
        console.log(`狗的品种是: ${pet.breed}`);
    } else if (isCat(pet)) {
        // pet 被收窄为 Cat
        pet.meow();
        console.log(`猫的颜色是: ${pet.color}`);
    }
}
```

`pet is Dog` 这种语法叫做类型谓词(type predicate)。函数返回 `true` 时,TypeScript 会自动将 `pet` 的类型收窄为 `Dog`。这种模式在处理复杂的联合类型时特别有用。

### 处理可选属性

自定义类型守卫也可以用于检测对象是否包含某个属性,这在处理配置对象或 API 响应时非常实用:

```typescript
interface UserResponse {
    id: number;
    name: string;
    email?: string;
    phone?: string;
}

function hasEmail(response: UserResponse): response is UserResponse & { email: string } {
    return response.email !== undefined;
}

function contactUser(user: UserResponse) {
    if (hasEmail(user)) {
        // 现在 user.email 是确定的 string 类型,不再是可选的
        sendEmail(user.email, "欢迎注册");
    } else if (user.phone) {
        sendSms(user.phone, "欢迎注册");
    }
}
```

---

## 五、实践建议与注意事项

在日常开发中,合理使用类型守卫能让代码更加类型安全。以下几点建议可以帮助你更好地运用这一机制。

首先,避免过度使用 `any` 类型。虽然 `any` 可以暂时消除类型错误,但它会让 TypeScript 的类型检查机制完全失效。如果不确定数据的类型结构,使用类型守卫配合 `unknown` 是更好的选择:

```typescript
function processUnknown(value: unknown) {
    if (typeof value === "string") {
        console.log(`字符串内容: ${value}`);
    } else if (typeof value === "number") {
        console.log(`数值大小: ${value}`);
    } else if (Array.isArray(value)) {
        console.log(`数组长度: ${value.length}`);
    } else {
        console.log("无法处理的类型");
    }
}

其次,注意空值和 null 的处理。在类型守卫中显式检查 nullundefined,可以让代码更加健壮:

function safeProcess(value: string | null | undefined) {
    if (value == null) {
        console.log("值为空");
        return;
    }
    // 现在 value 被收窄为 string
    console.log(`字符串长度: ${value.length}`);
}

最后,对于复杂的类型判断,考虑封装为可复用的类型守卫函数。这不仅能让主逻辑更加清晰,还能提高代码的可维护性。当某种类型判断逻辑在多处使用时,抽取为独立函数是明智的选择。

评论 (0)

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

扫一扫,手机查看

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