Skip to main content

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

Important

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): operationerrorHandlerlog

In this case, @log wraps @errorHandler, so:

  • If the method throws an error, @errorHandler will catch it
  • @log will 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): operationlogerrorHandler

In this case, @errorHandler wraps @log, so:

  • If the method throws an error, @log will 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.

tip

With decorator ordering, you can very flexibly configure method behavior just by swapping lines — what to log, which errors bubble up, validations, etc.