在现代后端开发中,日志记录和错误处理是构建健壮应用程序的关键组成部分。TypeScript作为JavaScript的超集,通过其强大的类型系统为我们提供了封装这些功能的绝佳工具。本文将探讨如何利用TypeScript的类型系统来构建类型安全的日志和错误处理机制。
为什么需要类型化的日志与错误处理
传统的JavaScript日志和错误处理往往缺乏结构化和类型安全,这可能导致:
- 不一致的日志格式,难以分析和聚合
- 错误信息缺乏必要的上下文
- 难以追踪和调试问题
- 团队成员间缺乏统一的错误处理约定
TypeScript的类型系统可以帮助我们解决这些问题,通过强制实施一致的日志结构和错误处理模式。
类型化的日志系统实现
1. 定义日志级别类型
typescript
type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'critical';
2. 创建基础日志接口
typescript
interface BaseLogEntry<T extends LogLevel = LogLevel> {
timestamp: Date;
level: T;
message: string;
context?: Record<string, unknown>;
stack?: string;
}
3. 实现类型化的日志记录器
typescript
class TypedLogger {
private log<T extends LogLevel>(level: T, message: string, context?: Record<string, unknown>): void {
const entry: BaseLogEntry<T> = {
timestamp: new Date(),
level,
message,
context,
};
if (level === 'error' || level === 'critical') {
entry.stack = new Error().stack;
}
// 实际日志输出逻辑,可以是控制台、文件或日志服务
console.log(JSON.stringify(entry));
}
debug(message: string, context?: Record<string, unknown>) {
this.log('debug', message, context);
}
error(message: string, context?: Record<string, unknown>) {
this.log('error', message, context);
}
// 其他级别方法...
}
类型化的错误处理
1. 定义基础错误类型
typescript
interface AppError<T extends string = string> {
code: T;
message: string;
details?: unknown;
stack?: string;
isOperational: boolean; // 区分操作错误与编程错误
}
2. 创建自定义错误类
typescript
class TypedError<T extends string> extends Error implements AppError<T> {
constructor(
public readonly code: T,
message: string,
public readonly details?: unknown,
public readonly isOperational: boolean = true
) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
Error.captureStackTrace(this, this.constructor);
}
toJSON(): AppError<T> {
return {
code: this.code,
message: this.message,
details: this.details,
stack: this.stack,
isOperational: this.isOperational,
};
}
}
3. 定义特定领域错误
typescript
type DatabaseErrorCode = 'CONNECTION_FAILED' | 'QUERY_FAILED' | 'TIMEOUT';
class DatabaseError extends TypedError<DatabaseErrorCode> {
constructor(code: DatabaseErrorCode, message: string, query?: string) {
super(code, message, { query });
}
}
错误处理中间件示例
在Express或Koa等框架中,可以创建类型化的错误处理中间件:
typescript
import { Request, Response, NextFunction } from 'express';
function errorHandler(err: unknown, req: Request, res: Response, next: NextFunction) {
const logger = new TypedLogger();
// 处理已知错误类型
if (err instanceof TypedError) {
logger.error(err.message, err.toJSON());
return res.status(getStatusCodeForError(err.code)).json({
error: err.toJSON(),
});
}
// 处理未知错误
logger.critical('Unhandled error', { error: err });
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred',
},
});
}
function getStatusCodeForError(code: string): number {
// 根据错误代码返回适当的HTTP状态码
// ...
}
最佳实践
- 统一错误代码:为所有可能的错误情况定义枚举或类型,确保一致性
- 丰富上下文:在错误和日志中包含足够的上下文信息
- 区分错误类型:区分操作错误(可预期的)和编程错误(bug)
- 结构化日志:确保日志条目遵循一致的结构,便于分析和搜索
- 类型安全:利用TypeScript确保所有日志和错误处理代码都经过类型检查
结论
通过TypeScript的类型系统封装日志和错误处理逻辑,我们可以构建更加健壮、可维护的后端应用程序。类型化的日志和错误不仅提高了代码质量,还使得调试和监控变得更加容易。这种模式特别适合大型团队和长期维护的项目,它强制执行一致的约定,减少了人为错误的可能性。
在实际项目中,你可以根据需求扩展这些基础实现,集成日志聚合服务(如ELK、Sentry等)或添加更复杂的错误恢复逻辑。关键是保持类型安全的同时提供足够的灵活性来处理各种场景。