Custom Decorators
In previous chapters, we used built-in decorators @operation and @daemon to extend service method functionality. But what if we need custom logic that will be applied to many methods? In this case, we can create our own decorators.
TypeScript Configuration
Sagun uses the legacy version of TypeScript decorators (Stage 2), not the new ECMAScript decorator standard. This is because the new standard doesn't support parameter decoration, which is necessary for DI implementation.
For decorators to work, make sure the experimentalDecorators option is enabled in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
Decorator Advantages
Decorators provide a powerful mechanism for extending functionality without changing the original method code:
- Reusability — a decorator written once can be applied to any number of methods
- Declarativeness — extension logic is explicitly indicated above the method, improving code readability
- Separation of concerns — the main method logic remains clean, and additional functionality is moved to the decorator
- Composition — decorators can be combined, layering functionality
Creating Your Own Decorator
Let's look at a practical example — we'll create a @log decorator that will log method calls to the console.
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const origin = descriptor.value;
// Wrap the original method
function* logged(...args: any[]) {
console.log(`▶ [${target.toString()}] ${key} called with args:`, args);
try {
// Call the original method preserving context
const result = yield* call([this, origin], ...args);
console.log(`✓ [${target.toString()}] ${key} returned:`, result);
return result;
} catch (error) {
console.error(`✗ [${target.toString()}] ${key} error:`, error);
throw error;
}
}
// Preserve original method properties (e.g., operation id)
descriptor.value = Object.assign(logged, origin);
return descriptor;
}
Now let's apply our decorator to a service from previous examples:
class OrderService extends Service {
toString() {
return "OrderService"
}
@log
@operation
*getOrders() {
return yield* call(fetchOrders);
}
@log
@daemon()
*addOrder() {
const id = getNewId();
yield* call(addOrder, {id, description: `Order ${id}`});
yield* call(this.getOrders);
}
}
Full example:
Open the browser console (F12 → Console) to see method call logs.
Decorator Order
Decorators in TypeScript are applied bottom to top — the bottommost decorator is applied first, then the next one wraps it, and so on. This is important to understand when composing multiple decorators.
Let's look at this with two decorators: @log and @errorHandler. The @errorHandler decorator catches errors and returns a fallback value instead of throwing an exception:
function errorHandler(fallback: any) {
return function(target: any, key: string, descriptor: PropertyDescriptor) {
const origin = descriptor.value;
function* caught(...args: any[]) {
try {
return yield* call([this, origin], ...args);
} catch (error) {
console.warn(`[${key}] error caught, returning fallback`);
return fallback;
}
}
descriptor.value = Object.assign(caught, origin);
return descriptor;
};
}
Variant 1: @log above @errorHandler
@log
@errorHandler([])
@operation
*getOrders() { ... }
Application order (bottom to top): operation → errorHandler → log
In this case, @log wraps @errorHandler, so:
- If the method throws an error,
@errorHandlerwill catch it @logwill see a successful result (fallback value)- Error will NOT be logged
Variant 2: @errorHandler above @log
@errorHandler([])
@log
@operation
*getOrders() { ... }
Application order (bottom to top): operation → log → errorHandler
In this case, @errorHandler wraps @log, so:
- If the method throws an error,
@logwill see it and log it - Then the error will "bubble up" to
@errorHandler, which will catch it - Error WILL be logged
Interactive Example
Try changing the order of @log and @errorHandler decorators in the OrderService.ts file and click the "Break loading" button. Notice the console — in one case the error is logged, in the other it's not.
With decorator ordering, you can very flexibly configure method behavior just by swapping lines — what to log, which errors bubble up, validations, etc.