TypeScript 类型守卫:typeof 与 instanceof
在 TypeScript 的类型系统中,类型守卫是让代码在运行时精准判断变量类型的机制。它解决的问题是:TypeScript 的类型推断是静态的,但实际运行时数据类型是动态的。当我们从外部获取数据(比如用户输入、API 返回)时,变量的类型在编译阶段往往是不确定的。这时候就需要类型守卫来帮助 TypeScript 理解"这个变量在特定条件下就是某种类型"。
typeof 和 instanceof 是最常用的两种类型守卫,它们分别适用于不同的场景。理解它们的区别和用法,是写出健壮 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.timeout 和 config.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 的处理。在类型守卫中显式检查 null 和 undefined,可以让代码更加健壮:
function safeProcess(value: string | null | undefined) {
if (value == null) {
console.log("值为空");
return;
}
// 现在 value 被收窄为 string
console.log(`字符串长度: ${value.length}`);
}
最后,对于复杂的类型判断,考虑封装为可复用的类型守卫函数。这不仅能让主逻辑更加清晰,还能提高代码的可维护性。当某种类型判断逻辑在多处使用时,抽取为独立函数是明智的选择。

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