在 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;
}
}
类型保护的最佳实践
-
优先使用内置类型保护:
typeof、instanceof、in操作符是最常用且最高效的类型保护方式 -
创建可重用的类型保护函数:对于复杂对象验证,创建自定义类型保护函数并在项目中复用
-
使用可区分联合模式:为复杂联合类型添加"标签"字段(如
type、kind或status),简化类型保护逻辑 -
结合错误处理:在类型保护中增加适当的错误处理或日志记录,便于调试
-
避免过度复杂的保护函数:保持类型保护函数简单和高效,避免嵌套太深的条件判断
-
为自定义类型保护编写测试:确保类型保护函数在各种边界条件下都能正确工作
常见陷阱与解决方案
陷阱 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-ts 或 zod 进行复杂的运行时验证:
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 进阶之道