日志与错误处理的类型封装

在现代后端开发中,日志记录和错误处理是构建健壮应用程序的关键组成部分。TypeScript作为JavaScript的超集,通过其强大的类型系统为我们提供了封装这些功能的绝佳工具。本文将探讨如何利用TypeScript的类型系统来构建类型安全的日志和错误处理机制。

为什么需要类型化的日志与错误处理

传统的JavaScript日志和错误处理往往缺乏结构化和类型安全,这可能导致:

  1. 不一致的日志格式,难以分析和聚合
  2. 错误信息缺乏必要的上下文
  3. 难以追踪和调试问题
  4. 团队成员间缺乏统一的错误处理约定

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状态码
  // ...
}

最佳实践

  1. 统一错误代码:为所有可能的错误情况定义枚举或类型,确保一致性
  2. 丰富上下文:在错误和日志中包含足够的上下文信息
  3. 区分错误类型:区分操作错误(可预期的)和编程错误(bug)
  4. 结构化日志:确保日志条目遵循一致的结构,便于分析和搜索
  5. 类型安全:利用TypeScript确保所有日志和错误处理代码都经过类型检查

结论

通过TypeScript的类型系统封装日志和错误处理逻辑,我们可以构建更加健壮、可维护的后端应用程序。类型化的日志和错误不仅提高了代码质量,还使得调试和监控变得更加容易。这种模式特别适合大型团队和长期维护的项目,它强制执行一致的约定,减少了人为错误的可能性。

在实际项目中,你可以根据需求扩展这些基础实现,集成日志聚合服务(如ELK、Sentry等)或添加更复杂的错误恢复逻辑。关键是保持类型安全的同时提供足够的灵活性来处理各种场景。