一、为什么需要类型安全的异常处理
在JavaScript的世界里,我们经常用try-catch来捕获异常,但这种方式有个很大的问题:它完全不关心错误的类型。你可能会在运行时才发现,原本以为是个网络错误,结果却是个类型错误。TypeScript作为JS的超集,给我们提供了类型系统,但默认的错误处理机制还是沿用JS那套,这就有点"穿着西装搬砖"的感觉。
来看个典型的JS风格错误处理:
function fetchUser(id) {
try {
// 这里可能抛出多种错误:网络错误、JSON解析错误、数据格式错误...
const response = await fetch(`/api/users/${id}`);
return await response.json();
} catch (error) {
// error的类型是any,我们完全不知道可能是什么
console.log("出错了,但不知道具体是什么错", error);
}
}
TypeScript可以做得更好。通过类型守卫和自定义错误类型,我们能创建一套类型安全的错误处理体系,让编译器也能帮我们检查错误处理是否完备。
二、构建类型化的错误体系
2.1 定义错误类型层次结构
首先我们需要建立一个错误类型体系。这就像给错误分门别类,让每种错误都有自己的"身份证"。
// 基础错误类型
class AppError extends Error {
constructor(message: string) {
super(message);
this.name = "AppError";
}
}
// 网络相关错误
class NetworkError extends AppError {
constructor(public statusCode: number, message: string) {
super(message);
this.name = "NetworkError";
}
}
// 业务逻辑错误
class BusinessError extends AppError {
constructor(public code: string, message: string) {
super(message);
this.name = "BusinessError";
}
}
// 数据验证错误
class ValidationError extends AppError {
constructor(public field: string, message: string) {
super(message);
this.name = "ValidationError";
}
}
2.2 类型守卫函数
有了错误类型,我们还需要一些"哨兵"来帮我们识别错误类型:
// 类型守卫函数
function isNetworkError(error: unknown): error is NetworkError {
return error instanceof NetworkError;
}
function isBusinessError(error: unknown): error is BusinessError {
return error instanceof BusinessError;
}
function isValidationError(error: unknown): error is ValidationError {
return error instanceof ValidationError;
}
这些函数不仅能在运行时检查类型,还能让TypeScript在编译时就知道具体的错误类型。
三、实战:类型安全的错误处理模式
3.1 模式一:明确错误返回类型
我们可以让函数明确返回可能发生的错误类型:
async function fetchUserProfile(
userId: string
): Promise<UserProfile | NetworkError | BusinessError> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
// 明确返回NetworkError
return new NetworkError(response.status, "请求用户数据失败");
}
const data = await response.json();
if (!validateUserProfile(data)) {
// 明确返回BusinessError
return new BusinessError("INVALID_PROFILE", "用户数据格式不正确");
}
return data as UserProfile;
} catch (error) {
// 捕获未预期的错误,转换为已知错误类型
if (error instanceof Error) {
return new NetworkError(500, error.message);
}
return new NetworkError(500, "未知错误");
}
}
使用时可以这样处理:
const result = await fetchUserProfile("123");
if (result instanceof NetworkError) {
// 现在result被识别为NetworkError
console.error(`网络错误: ${result.statusCode}`);
} else if (result instanceof BusinessError) {
// 现在result被识别为BusinessError
console.error(`业务错误: ${result.code}`);
} else {
// 这里result一定是UserProfile
displayUserProfile(result);
}
3.2 模式二:使用Either模式
函数式编程中的Either模式也很适合处理这种情况:
type Either<L, R> = { kind: "left"; value: L } | { kind: "right"; value: R };
async function fetchOrder(
orderId: string
): Promise<Either<AppError, Order>> {
try {
const response = await fetch(`/api/orders/${orderId}`);
if (!response.ok) {
return {
kind: "left",
value: new NetworkError(response.status, "订单请求失败")
};
}
const data = await response.json();
return { kind: "right", value: data as Order };
} catch (error) {
return {
kind: "left",
value: new NetworkError(500, error instanceof Error ? error.message : "未知错误")
};
}
}
使用Either模式时,处理方式更加结构化:
const result = await fetchOrder("456");
if (result.kind === "left") {
// 错误处理
console.error(result.value.message);
} else {
// 成功处理
processOrder(result.value);
}
四、高级模式与最佳实践
4.1 错误转换与封装
在实际应用中,我们经常需要将底层错误转换为高层错误:
class UserService {
async getUser(id: string): Promise<User | AppError> {
try {
const response = await this.apiClient.get(`/users/${id}`);
// 将API特定的错误转换为业务错误
if (response.error) {
return this.transformApiError(response.error);
}
return response.data;
} catch (error) {
// 封装所有未处理错误
return new AppError("USER_SERVICE_ERROR", "获取用户失败");
}
}
private transformApiError(apiError: ApiError): AppError {
switch (apiError.code) {
case "USER_NOT_FOUND":
return new BusinessError("USER_NOT_FOUND", "用户不存在");
case "INVALID_INPUT":
return new ValidationError(apiError.field, apiError.message);
default:
return new NetworkError(500, apiError.message);
}
}
}
4.2 异步错误收集
在处理多个并行操作时,我们可以收集所有错误:
async function fetchMultipleResources() {
const [usersResult, productsResult] = await Promise.all([
fetchUsers().catch(e => e),
fetchProducts().catch(e => e)
]);
const errors: AppError[] = [];
if (usersResult instanceof AppError) errors.push(usersResult);
if (productsResult instanceof AppError) errors.push(productsResult);
if (errors.length > 0) {
// 处理收集到的所有错误
throw new AggregateError(errors, "多个资源加载失败");
}
return {
users: usersResult as User[],
products: productsResult as Product[]
};
}
五、应用场景与技术考量
5.1 适用场景
这种模式特别适合:
- 大型TypeScript项目,需要严格类型安全
- 需要明确区分业务错误和技术错误的系统
- 前后端协作项目,需要统一错误处理规范
- 需要详细错误日志和监控的应用
5.2 技术优缺点
优点:
- 编译时就能发现错误处理遗漏
- 代码自文档化,从类型就知道可能发生的错误
- 错误处理逻辑更加结构化
- 便于统一错误报告和监控
- 与TypeScript类型系统完美集成
缺点:
- 需要前期设计错误类型体系
- 对小型项目可能显得过于复杂
- 需要团队成员都遵守这套规范
- 某些情况下会增加一些样板代码
5.3 注意事项
- 不要过度设计,根据项目规模选择合适的粒度
- 保持错误类型的稳定,避免频繁修改
- 为常见错误类型编写工具函数和类型守卫
- 考虑错误国际化需求
- 记录足够的错误上下文信息
- 注意错误对象的序列化问题
六、总结
TypeScript的类型系统为我们提供了重新思考错误处理的机会。通过建立类型化的错误体系,我们能让错误处理更加可靠和可维护。虽然需要一些前期投入,但在复杂项目中,这种投入很快就会通过减少运行时错误和提高开发效率得到回报。
记住,好的错误处理不是事后添加的,而应该是一开始就设计好的。类型安全的错误处理不仅能捕获更多错误,还能让你的代码更加自描述,让团队成员更容易理解各种可能的执行路径。
评论