Engineer a Custom Logging Method Decorator in TypeScript
Unlock advanced code monitoring and enhance the diagnostic capabilities of your TypeScript applications. We navigate the intricate landscape of modern software architecture, where visibility into execution flow is paramount. This exploration delves into the strategic implementation of custom method decorators, providing an unparalleled mechanism for logging crucial operational data—from argument capture to return value analysis and error detection—without cluttering your core business logic.
Traditional logging approaches often entangle concerns, diminishing code readability and maintainability. Forge a superior path by leveraging TypeScript's powerful decorator syntax. This article empowers you to dissect the decorator pattern, build a robust logging solution, and integrate it seamlessly, ensuring clean separation of concerns. This strategy is a cornerstone of effective Object-Oriented Programming and Modern JavaScript Integration, fundamentally transforming how we approach cross-cutting concerns. Prepare to elevate your development practice by mastering a technique that promises unparalleled clarity and control over your application's runtime behavior.
Decoding TypeScript Decorators: The Foundational Blueprint
Deciphering TypeScript decorators begins with comprehending their core mechanics. Decorators are specialized declarations that can be attached to classes, methods, accessors, properties, or parameters. They are functions executed at declaration time, empowering us to modify or augment the behavior of the code they decorate without altering its intrinsic logic. This metaprogramming capability injects cross-cutting concerns with surgical precision, fostering cleaner, more modular architectures.
For method decorators, the signature is critical: a function receiving three arguments. First, target, which points to the prototype of the class for an instance method or the constructor function for a static method. Second, propertyKey, the string name of the method being decorated. Third, and most importantly, descriptor, the property descriptor for the method. This descriptor provides access to the method's configuration, including its value (the actual function), writable, enumerable, and configurable flags. Manipulating descriptor.value is the gateway to injecting custom logic before, after, or around the original method's execution.
The common pattern involves a 'decorator factory'—a function that accepts parameters and returns the actual decorator function. This mechanism transforms decorators from static annotations into highly configurable tools. By returning a function closure, the factory ensures that configuration parameters are available to the decorator when it executes. Grasping this foundational blueprint is paramount; it unlocks the full potential of dynamic behavior modification and sets the stage for constructing sophisticated utilities like our custom logging solution. We dissect the provided code to observe how the simpleLog factory returns a decorator that wraps the original method, injecting basic logging calls before and after its execution. This initial step activates our understanding of the decorator lifecycle and immediate impact.
/*
tsconfig.json must have:
"experimentalDecorators": true,
"emitDecoratorMetadata": true
*/
// Step 1: Define a simple decorator factory function
// This function returns the actual decorator, allowing it to accept parameters.
function simpleLog(message: string) {
// The actual decorator function executed at declaration time.
// target: The prototype of the class for an instance method, or the constructor function for a static method.
// propertyKey: The name of the method.
// descriptor: The property descriptor for the method.
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(`Decorator activated for method: ${String(propertyKey)} with message: ${message}`);
// Store the original method implementation.
const originalMethod = descriptor.value;
// Override the method with a new function that adds custom logic.
descriptor.value = function (...args: any[]) {
console.log(`Executing method '${String(propertyKey)}' with args: ${JSON.stringify(args)}`);
// Call the original method and return its result.
const result = originalMethod.apply(this, args);
console.log(`Method '${String(propertyKey)}' returned: ${JSON.stringify(result)}`);
return result;
};
// Return the modified descriptor.
return descriptor;
};
}
// Step 2: Apply the decorator to a class method
class Calculator {
@simpleLog("Performing calculation")
add(a: number, b: number): number {
return a + b;
}
@simpleLog("Processing data")
subtract(x: number, y: number): number {
return x - y;
}
}
// Step 3: Instantiate the class and invoke the decorated method
const calculator = new Calculator();
calculator.add(5, 3);
calculator.subtract(10, 4);
Architecting the Logging Mechanism: Precision and Context
Constructing an effective logging mechanism demands foresight and precise execution. We must determine what information is valuable for diagnostics: the method name, its class, input arguments, returned values, execution duration, and any exceptions thrown. The decorator serves as a powerful interception point, enabling us to capture this data comprehensively without intruding on the method's core functionality. This strategy encapsulates logging as a cross-cutting concern, adhering to the Single Responsibility Principle and elevating code clarity.
Our logging decorator needs to wrap the original method's execution. Within the new method body, we initiate logging before calling the original function. Capturing this and arguments at this stage is crucial, preserving the method's original context. The originalMethod.apply(this, args) call is indispensable; it ensures the method executes with its correct this context and arguments, mimicking its natural invocation. After execution, we must capture the return value or handle potential errors, then log the final state.
A critical consideration emerges with asynchronous methods returning Promises. A simple synchronous wrap will not suffice. We must identify if the method returns a Promise and, if so, chain .then() and .catch() handlers to capture the resolved value or the rejected error. This guarantees comprehensive logging for both synchronous and asynchronous operations, a non-negotiable requirement for modern applications. The provided code demonstrates this duality, ensuring our logging mechanism is robust enough to operate across varying method types. This meticulous architectural approach forms the bedrock of a truly effective diagnostic tool.
/*
tsconfig.json must have:
"experimentalDecorators": true,
"emitDecoratorMetadata": true
*/
// Define an interface for structured log messages (optional, but good practice)
interface LogMessage {
timestamp: string;
level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
className?: string;
methodName: string;
arguments?: any[];
returnValue?: any;
executionTimeMs?: number;
error?: string;
message: string;
}
// Utility for logging (could be replaced with a proper logging library)
function logToConsole(logEntry: LogMessage) {
const formattedArgs = logEntry.arguments ? JSON.stringify(logEntry.arguments) : '[]';
const formattedReturn = logEntry.returnValue ? JSON.stringify(logEntry.returnValue) : 'N/A';
const formattedError = logEntry.error ? `Error: ${logEntry.error}` : '';
const formattedTime = logEntry.executionTimeMs ? `${logEntry.executionTimeMs.toFixed(2)}ms` : '';
console.log(`[${logEntry.timestamp}] [${logEntry.level}] [${logEntry.className || 'Unknown'}.${logEntry.methodName}] - ${logEntry.message}` +
` Args: ${formattedArgs} Return: ${formattedReturn} Time: ${formattedTime} ${formattedError}`);
}
// Step 1: Design the logging method decorator factory
function LogMethod() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const className = target.constructor.name;
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const timestamp = new Date().toISOString();
let returnValue: any;
let error: any;
const startTime = performance.now();
try {
// Log method entry
logToConsole({
timestamp: timestamp,
level: 'INFO',
className: className,
methodName: String(propertyKey),
arguments: args,
message: 'Method invoked'
});
// Execute the original method
returnValue = originalMethod.apply(this, args);
// Handle asynchronous methods (Promises)
if (returnValue instanceof Promise) {
return returnValue.then((result: any) => {
const endTime = performance.now();
const executionTimeMs = endTime - startTime;
logToConsole({
timestamp: new Date().toISOString(),
level: 'INFO',
className: className,
methodName: String(propertyKey),
arguments: args,
returnValue: result,
executionTimeMs: executionTimeMs,
message: 'Method completed (async)'
});
return result;
}).catch((e: any) => {
const endTime = performance.now();
const executionTimeMs = endTime - startTime;
logToConsole({
timestamp: new Date().toISOString(),
level: 'ERROR',
className: className,
methodName: String(propertyKey),
arguments: args,
executionTimeMs: executionTimeMs,
error: e.message || 'Unknown error',
message: 'Method failed (async)'
});
throw e; // Re-throw the error to maintain program flow
});
} else {
// Handle synchronous methods
const endTime = performance.now();
const executionTimeMs = endTime - startTime;
logToConsole({
timestamp: new Date().toISOString(),
level: 'INFO',
className: className,
methodName: String(propertyKey),
arguments: args,
returnValue: returnValue,
executionTimeMs: executionTimeMs,
message: 'Method completed (sync)'
});
return returnValue;
}
} catch (e: any) {
const endTime = performance.now();
const executionTimeMs = endTime - startTime;
error = e;
logToConsole({
timestamp: new Date().toISOString(),
level: 'ERROR',
className: className,
methodName: String(propertyKey),
arguments: args,
executionTimeMs: executionTimeMs,
error: error.message || 'Unknown error',
message: 'Method failed (sync)'
});
throw error; // Re-throw the error to maintain program flow
}
};
return descriptor;
};
}
// Step 2: Apply the decorator to a class with a mix of sync and async methods
class DataProcessor {
@LogMethod()
processSync(data: string, id: number): string {
if (id < 0) {
throw new Error("ID cannot be negative.");
}
return `Processed: ${data}-${id}`;
}
@LogMethod()
async fetchUser(userId: number): Promise<{ id: number; name: string }> {
// Simulate an async operation
await new Promise(resolve => setTimeout(resolve, 100));
if (userId === 999) {
throw new Error("User not found.");
}
return { id: userId, name: `User_${userId}` };
}
}
// Step 3: Test the decorated methods
const processor = new DataProcessor();
console.log("\n--- Testing successful synchronous call ---");
processor.processSync("report", 123);
console.log("\n--- Testing failed synchronous call ---");
try {
processor.processSync("invalid", -1);
} catch (e) {
// Expected error handling
}
console.log("\n--- Testing successful asynchronous call ---");
processor.fetchUser(42).then(user => {
// console.log(`Fetched user: ${user.name}`);
});
console.log("\n--- Testing failed asynchronous call ---");
processor.fetchUser(999).catch(err => {
// console.log(`Failed to fetch user: ${err.message}`);
});
Refining the Decorator for Practicality and Robust Error Handling
Optimizing our logging decorator involves introducing configurability and reinforcing error handling. A truly practical decorator permits developers to control its behavior on a per-method basis. We achieve this by enhancing the decorator factory to accept an options object. This object empowers users to selectively enable or disable logging of arguments, return values, execution time, and even error messages. Such granular control prevents log spam, tailors output to specific debugging needs, and mitigates potential performance overhead for less critical operations. Activating this flexibility transforms the decorator from a fixed utility into a versatile diagnostic powerhouse.
Precision timing is another critical enhancement. Utilizing performance.now() provides high-resolution timestamps, enabling us to accurately measure the execution duration of methods. By capturing timestamps before and after the method's invocation, we derive precise execution times, invaluable for performance profiling and identifying bottlenecks. This data point offers objective metrics for optimizing critical paths within the application. We integrate this timing logic directly into the decorator, ensuring it's applied consistently wherever the decorator is used.
Robust error handling within the decorator is non-negotiable. Encapsulating the original method call within a try...catch block is mandatory. This ensures that any exceptions thrown by the decorated method are caught, logged with appropriate context (method name, arguments, timestamp, error message), and critically, re-thrown. Re-throwing the error maintains the expected program flow; it allows higher-level error handling mechanisms to intercept and process the exception as if the decorator were not present. Failing to re-throw breaks the application's error propagation chain, leading to silent failures. This meticulous approach to error management solidifies the decorator's reliability and diagnostic value, securing our system against unseen failures.
/*
tsconfig.json must have:
"experimentalDecorators": true,
"emitDecoratorMetadata": true
*/
// Re-use the LogMessage interface and logToConsole utility from previous step for consistency.
interface LogMessage {
timestamp: string;
level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
className?: string;
methodName: string;
arguments?: any[];
returnValue?: any;
executionTimeMs?: number;
error?: string;
message: string;
}
function logToConsole(logEntry: LogMessage) {
const formattedArgs = logEntry.arguments ? JSON.stringify(logEntry.arguments) : '[]';
const formattedReturn = logEntry.returnValue ? JSON.stringify(logEntry.returnValue) : 'N/A';
const formattedError = logEntry.error ? `Error: ${logEntry.error}` : '';
const formattedTime = logEntry.executionTimeMs ? `${logEntry.executionTimeMs.toFixed(2)}ms` : '';
console.log(`[${logEntry.timestamp}] [${logEntry.level}] [${logEntry.className || 'Unknown'}.${logEntry.methodName}] - ${logEntry.message}` +
` Args: ${formattedArgs} Return: ${formattedReturn} Time: ${formattedTime} ${formattedError}`);
}
// Step 1: Enhance the LogMethod decorator with configurable options
interface LogOptions {
logArguments?: boolean;
logReturnValue?: boolean;
logExecutionTime?: boolean;
logErrors?: boolean;
level?: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
}
function LogMethod(options: LogOptions = {}) {
const {
logArguments = true,
logReturnValue = true,
logExecutionTime = true,
logErrors = true,
level = 'INFO'
} = options;
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const className = target.constructor.name;
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const timestamp = new Date().toISOString();
const startTime = logExecutionTime ? performance.now() : 0;
// Prepare initial log entry
const initialLog: LogMessage = {
timestamp: timestamp,
level: level,
className: className,
methodName: String(propertyKey),
message: 'Method invoked'
};
if (logArguments) initialLog.arguments = args;
logToConsole(initialLog);
try {
const methodResult = originalMethod.apply(this, args);
// Handle asynchronous methods
if (methodResult instanceof Promise) {
return methodResult.then((result: any) => {
const endTime = logExecutionTime ? performance.now() : 0;
const finalLog: LogMessage = {
timestamp: new Date().toISOString(),
level: level,
className: className,
methodName: String(propertyKey),
message: 'Method completed (async)'
};
if (logArguments) finalLog.arguments = args;
if (logReturnValue) finalLog.returnValue = result;
if (logExecutionTime) finalLog.executionTimeMs = endTime - startTime;
logToConsole(finalLog);
return result;
}).catch((e: any) => {
const endTime = logExecutionTime ? performance.now() : 0;
if (logErrors) {
const errorLog: LogMessage = {
timestamp: new Date().toISOString(),
level: 'ERROR',
className: className,
methodName: String(propertyKey),
message: 'Method failed (async)'
};
if (logArguments) errorLog.arguments = args;
if (logExecutionTime) errorLog.executionTimeMs = endTime - startTime;
errorLog.error = e.message || 'Unknown error';
logToConsole(errorLog);
}
throw e; // Crucial: re-throw the error to propagate it
});
} else {
// Handle synchronous methods
const endTime = logExecutionTime ? performance.now() : 0;
const finalLog: LogMessage = {
timestamp: new Date().toISOString(),
level: level,
className: className,
methodName: String(propertyKey),
message: 'Method completed (sync)'
};
if (logArguments) finalLog.arguments = args;
if (logReturnValue) finalLog.returnValue = methodResult;
if (logExecutionTime) finalLog.executionTimeMs = endTime - startTime;
logToConsole(finalLog);
return methodResult;
}
} catch (e: any) {
const endTime = logExecutionTime ? performance.now() : 0;
if (logErrors) {
const errorLog: LogMessage = {
timestamp: new Date().toISOString(),
level: 'ERROR',
className: className,
methodName: String(propertyKey),
message: 'Method failed (sync)'
};
if (logArguments) errorLog.arguments = args;
if (logExecutionTime) errorLog.executionTimeMs = endTime - startTime;
errorLog.error = e.message || 'Unknown error';
logToConsole(errorLog);
}
throw e; // Crucial: re-throw the error to propagate it
}
};
return descriptor;
};
}
// Step 2: Test the refined decorator with specific configurations
class ServiceLayer {
@LogMethod({ logArguments: false, logExecutionTime: true, level: 'DEBUG' })
fetchConfig(key: string): string {
// Simulate work
for (let i = 0; i < 1000000; i++) {}
return `Config for ${key}`;
}
@LogMethod({ logReturnValue: false, logErrors: true })
async saveData(payload: any): Promise<boolean> {
await new Promise(resolve => setTimeout(resolve, 50));
if (!payload || Object.keys(payload).length === 0) {
throw new Error("Payload cannot be empty.");
}
return true;
}
@LogMethod({ logArguments: true, logReturnValue: true, logErrors: false })
calculateHash(input: string): string {
if (!input) {
throw new Error("Input cannot be null for hash calculation.");
}
return `HASH-${input.length}`; // Simplified hash
}
}
const service = new ServiceLayer();
console.log("\n--- Testing fetchConfig (no args, time) ---");
service.fetchConfig("database.url");
console.log("\n--- Testing saveData (no return, errors) ---");
service.saveData({ user: "Alice" });
try {
service.saveData({});
} catch (e) {
// Expected error
}
console.log("\n--- Testing calculateHash (args, return, no errors logged) ---");
service.calculateHash("important_data");
try {
service.calculateHash("");
} catch (e) {
// Error occurs, but decorator won't log it due to logErrors: false
console.log("Error caught externally (not logged by decorator):", e.message);
}
Deploying and Optimizing Custom Decorators: Strategic Integration
Deploying custom method decorators requires meticulous attention to both compiler settings and strategic application. To enable decorator syntax in TypeScript, we must configure tsconfig.json. Activate "experimentalDecorators": true to parse the decorator syntax. Furthermore, for advanced scenarios involving reflection and metadata, set "emitDecoratorMetadata": true. These configurations are foundational; neglecting them renders decorators non-functional. We also ensure our target environment supports the necessary JavaScript features, typically ES2015 or newer, and include "lib": ["ES2020", "DOM"] to access browser APIs like performance.now() if operating in a Node.js context, or simply rely on browser environments to provide it.
Integrating the custom logging decorator across an application demands strategic thought. Identify key methods within critical services, controllers, or data access layers where visibility is paramount. Apply the @LogMethod() decorator to these methods, leveraging its configurable options to tailor logging output. For high-frequency, performance-sensitive methods, we might disable argument or return value logging to minimize overhead. For crucial business logic, comprehensive logging including error details and execution time provides invaluable diagnostic trails.
Optimize decorator usage by adhering to best practices. Avoid over-decorating; not every method requires intricate logging. Instead, pinpoint methods that represent significant entry points, state transitions, or potential failure points. Consider the performance implications: while decorators offer elegance, they introduce a layer of indirection. For extremely hot code paths, manual logging might still be marginally faster. However, the maintainability and cleanliness gained by decorators often outweigh this minor performance delta. Rigorously test decorated methods to ensure the logging logic functions correctly and does not inadvertently alter the method's original behavior or error propagation. This disciplined approach guarantees that our custom logging decorator enhances, rather than hinders, the application's overall robustness and clarity.
/*
tsconfig.json must have:
"experimentalDecorators": true,
"emitDecoratorMetadata": true
*/
// Re-use the LogMessage interface, logToConsole utility, and LogMethod decorator for consistency.
interface LogMessage {
timestamp: string;
level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
className?: string;
methodName: string;
arguments?: any[];
returnValue?: any;
executionTimeMs?: number;
error?: string;
message: string;
}
function logToConsole(logEntry: LogMessage) {
const formattedArgs = logEntry.arguments ? JSON.stringify(logEntry.arguments) : '[]';
const formattedReturn = logEntry.returnValue ? JSON.stringify(logEntry.returnValue) : 'N/A';
const formattedError = logEntry.error ? `Error: ${logEntry.error}` : '';
const formattedTime = logEntry.executionTimeMs ? `${logEntry.executionTimeMs.toFixed(2)}ms` : '';
console.log(`[${logEntry.timestamp}] [${logEntry.level}] [${logEntry.className || 'Unknown'}.${logEntry.methodName}] - ${logEntry.message}` +
` Args: ${formattedArgs} Return: ${formattedReturn} Time: ${formattedTime} ${formattedError}`);
}
interface LogOptions {
logArguments?: boolean;
logReturnValue?: boolean;
logExecutionTime?: boolean;
logErrors?: boolean;
level?: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
}
function LogMethod(options: LogOptions = {}) {
const {
logArguments = true,
logReturnValue = true,
logExecutionTime = true,
logErrors = true,
level = 'INFO'
} = options;
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const className = target.constructor.name;
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const timestamp = new Date().toISOString();
const startTime = logExecutionTime ? performance.now() : 0;
const initialLog: LogMessage = {
timestamp: timestamp,
level: level,
className: className,
methodName: String(propertyKey),
message: 'Method invoked'
};
if (logArguments) initialLog.arguments = args;
logToConsole(initialLog);
try {
const methodResult = originalMethod.apply(this, args);
if (methodResult instanceof Promise) {
return methodResult.then((result: any) => {
const endTime = logExecutionTime ? performance.now() : 0;
const finalLog: LogMessage = {
timestamp: new Date().toISOString(),
level: level,
className: className,
methodName: String(propertyKey),
message: 'Method completed (async)'
};
if (logArguments) finalLog.arguments = args;
if (logReturnValue) finalLog.returnValue = result;
if (logExecutionTime) finalLog.executionTimeMs = endTime - startTime;
logToConsole(finalLog);
return result;
}).catch((e: any) => {
const endTime = logExecutionTime ? performance.now() : 0;
if (logErrors) {
const errorLog: LogMessage = {
timestamp: new Date().toISOString(),
level: 'ERROR',
className: className,
methodName: String(propertyKey),
message: 'Method failed (async)'
};
if (logArguments) errorLog.arguments = args;
if (logExecutionTime) errorLog.executionTimeMs = endTime - startTime;
errorLog.error = e.message || 'Unknown error';
logToConsole(errorLog);
}
throw e;
});
} else {
const endTime = logExecutionTime ? performance.now() : 0;
const finalLog: LogMessage = {
timestamp: new Date().toISOString(),
level: level,
className: className,
methodName: String(propertyKey),
message: 'Method completed (sync)'
};
if (logArguments) finalLog.arguments = args;
if (logReturnValue) finalLog.returnValue = methodResult;
if (logExecutionTime) finalLog.executionTimeMs = endTime - startTime;
logToConsole(finalLog);
return methodResult;
}
} catch (e: any) {
const endTime = logExecutionTime ? performance.now() : 0;
if (logErrors) {
const errorLog: LogMessage = {
timestamp: new Date().toISOString(),
level: 'ERROR',
className: className,
methodName: String(propertyKey),
message: 'Method failed (sync)'
};
if (logArguments) errorLog.arguments = args;
if (logExecutionTime) errorLog.executionTimeMs = endTime - startTime;
errorLog.error = e.message || 'Unknown error';
logToConsole(errorLog);
}
throw e;
}
};
return descriptor;
};
}
// Step 1: Complete class example using the decorated methods
class PaymentGatewayService {
private transactions: { id: string; amount: number; status: string }[] = [];
@LogMethod()
initiateTransaction(id: string, amount: number): { id: string; status: string } {
if (amount <= 0) {
throw new Error("Transaction amount must be positive.");
}
const transaction = { id, amount, status: "PENDING" };
this.transactions.push(transaction);
return { id: transaction.id, status: transaction.status };
}
@LogMethod({ logArguments: false, logExecutionTime: true })
async processPayment(transactionId: string): Promise<string> {
await new Promise(resolve => setTimeout(resolve, 200)); // Simulate async processing
const transaction = this.transactions.find(t => t.id === transactionId);
if (!transaction) {
throw new Error(`Transaction ${transactionId} not found.`);
}
transaction.status = "COMPLETED";
return "Payment successful.";
}
@LogMethod({ logReturnValue: false, logErrors: true, level: 'WARN' })
auditTransaction(transactionId: string): boolean {
const transaction = this.transactions.find(t => t.id === transactionId);
if (!transaction) {
console.warn(`Audit failed: Transaction ${transactionId} does not exist.`);
return false;
}
// Simulate audit logic
return transaction.status === "COMPLETED";
}
@LogMethod({ logArguments: false, logReturnValue: false, logExecutionTime: true, logErrors: false, level: 'DEBUG' })
getDashboardStats(): { totalTransactions: number; completed: number } {
const totalTransactions = this.transactions.length;
const completed = this.transactions.filter(t => t.status === "COMPLETED").length;
return { totalTransactions, completed };
}
}
// Step 2: Instantiate and use the service
const gateway = new PaymentGatewayService();
console.log("\n--- Initiating Transactions ---");
const t1 = gateway.initiateTransaction("TXN001", 100);
const t2 = gateway.initiateTransaction("TXN002", 250);
try {
gateway.initiateTransaction("TXN003", 0);
} catch (e) { /* Expected */ }
console.log("\n--- Processing Payments ---");
gateway.processPayment(t1.id).then(res => console.log(res));
gateway.processPayment(t2.id).then(res => console.log(res));
gateway.processPayment("NON_EXISTENT").catch(e => console.error(`Error processing: ${e.message}`));
console.log("\n--- Auditing Transactions ---");
gateway.auditTransaction(t1.id);
gateway.auditTransaction("TXN005"); // Will trigger a WARN log and return false
console.log("\n--- Getting Dashboard Stats ---");
gateway.getDashboardStats();
// Example tsconfig.json configuration for decorators
/*
{
"compilerOptions": {
"target": "ES2020", // Or newer, like ESNext
"module": "commonjs", // Or ESNext, NodeNext
"lib": ["ES2020", "DOM"], // Include DOM for performance.now
"experimentalDecorators": true, // Crucial: Enables decorator syntax
"emitDecoratorMetadata": true, // Recommended for advanced decorator usage, especially with reflection
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
*/
Key Takeaways
Understanding TypeScript Decorators
TypeScript decorators are functions that allow metadata attachment or behavior modification to classes, methods, accessors, properties, or parameters. Method decorators receive target (class prototype/constructor), propertyKey (method name), and descriptor (method's property descriptor). They execute at declaration time, enabling powerful metaprogramming by wrapping or replacing original method implementations.
Building a Custom Logging Decorator
We construct a logging decorator using a 'decorator factory' pattern, allowing configurable options (e.g., logArguments, logReturnValue, logExecutionTime, logErrors). The decorator wraps the original method, capturing arguments, return values, execution time via performance.now(), and handling errors with try...catch. Crucially, it re-throws errors to maintain program flow and handles asynchronous methods by chaining .then() and .catch() to Promises.
Key Benefits and Best Practices
Custom logging decorators promote a clean separation of concerns, moving logging logic out of business methods. This enhances code readability and maintainability. When deploying, activate "experimentalDecorators": true and "emitDecoratorMetadata": true in tsconfig.json. Optimize by selectively applying decorators to critical methods, considering performance overhead for high-frequency operations, and thoroughly testing to ensure correct behavior and error propagation.
FAQ
-
Why use method decorators for logging instead of manual
console.logcalls?
Method decorators enforce a powerful separation of concerns. They extract cross-cutting concerns like logging from the core business logic, preventing code entanglement. This leads to cleaner, more readable, and significantly more maintainable code. Manual calls clutter methods, making them harder to reason about and modify. Decorators centralize logging logic, allowing consistent application and easy modification across multiple methods without touching their implementations.
-
Can a custom logging decorator handle asynchronous methods (Promises)?
Absolutely. A well-engineered logging decorator must account for asynchronous operations. We achieve this by checking if the return value of the original method is an instance of
Promise. If it is, we chain.then()and.catch()clauses to the Promise. This allows the decorator to capture the resolved value or the rejected error after the asynchronous operation completes, ensuring comprehensive logging for both successful and failed asynchronous executions. -
What are the performance implications of using decorators for logging?
Decorators introduce a minor overhead due to the additional function calls and context wrapping. This overhead is generally negligible for most application scenarios. However, for extremely high-frequency, performance-critical methods (e.g., tight loops, real-time data processing), this indirection could accumulate. Always profile your application if performance becomes a concern. The benefits in maintainability and code clarity typically outweigh the minimal performance cost.
-
How can I configure the logging output (e.g., to a file, database) from the decorator?
The decorator factory pattern is designed for this. You can pass configuration options to the decorator when you apply it (e.g.,
@LogMethod({ destination: 'file', level: 'DEBUG' })). The decorator's internal logging utility (logToConsolein our example) can then be adapted to use a more sophisticated logging library (like Winston or Pino) that supports various output destinations. This allows centralized control over where logs are sent without modifying individual decorated methods.