Logging values to the console is an integral part of my daily work (and probably yours too). We can place all sorts of messages in the console: validating data, displaying information after a process has completed, or providing developer warnings are just some of the uses of the console object.
All these operations and many more can be performed using only the methods available for this object. These include:
- console.log
- console.error
- console.debug
- console.info
- console.warn
In this post, I’d like to present a more advanced way to manage logging in your application. This approach will bring order to your code and allow us to handle such a trivial operation with a separate module that we can modify as needed. In many projects, I’ve encountered the use of the no-console rule in ESlint, which resulted in scattered exceptions for this rule: // eslint-disable no-console. Thanks to the approach I’ve presented, this rule can be disabled in a single place in the code, because in the application, we won’t be using the console object, but rather a dedicated class.
Preparation
First, let’s start with interface and enum declarations.
interface LoggerInterface { debug: (message: string, data?: unknown) => void; info: (message: string, data?: unknown) => void; warning: (message: string, data?: unknown) => void; error: (message: string, error?: Error, data?: unknown) => void; } enum LogLevel { Debug, Info, Warning, Error, }Notice that I’m not using the any type—I’ve replaced it with the more specific unknown. The next step is to declare the LogEvent helper class.
class LogEvent<T = unknown> { public readonly id = crypto.randomUUID(); constructor( public readonly level: LogLevel, public readonly message: string, public readonly data?: T, public readonly error?: T ) { this.level = level; this.message = message; this.data = data; this.error = error; } }To assign the id, I use the crypto object available in the browser. This class is used to create the structure of the message object that we log to the console. This message will not only have a body. It will also contain additional information:
- level - compatible with the LogLevel enum,
- message - the actual message body,
- data - data we can attach to the message,
- error - if level is of type Error, this information will be attached to the object.
Finally, the most important class, which is used to gather information and use the appropriate method on the console object.
class Logger implements LoggerInterface { private readonly logToConsoleEnabled: boolean; constructor(logToConsoleEnabled: boolean) { this.logToConsoleEnabled = logToConsoleEnabled; } private logToConsole(event: LogEvent) { switch(event.level) { case LogLevel.Info: console.info(event.message, event); break; case LogLevel.Warning: console.warn(event.message, event); break; case LogLevel.Error: console.error(event.message, event); break; case LogLevel.Debug: console.debug(event.message, event); break; default: console.log(event.message, event); } } private log<T = unknown>(event: LogEvent<T>) { if (this.logToConsoleEnabled || event.level && event.level > LogLevel.Error) { this.logToConsole(event); }} private debug<T>(message: string, data: unknown): void { this.log(new LogEvent(LogLevel.Debug, message, data)) } error<T>(message: string, error: T, data: unknown): void { this.log(new LogEvent(LogLevel.Error, message, data, error)) } info<T>(message: string, data: unknown): void { this.log(new LogEvent(LogLevel.Info, message, data)) } warning<T>(message: string, data: unknown): void { this.log(new LogEvent(LogLevel.Warning, message, data)) } } const logger = new Logger(true);The class accepts only one parameter in its constructor, logToConsoleEnabled. This can be used to specify the environment in which notifications should be displayed in the console. A common use case is hiding logs in a production environment.
When using an instance of the class, we only use public methods, i.e., methods that use specific methods of the console object. Below are examples of displayed messages using this class.
.png)


