Door 23 | JS Adventskalender
Skip to content

Door 23

Published: at 07:00 AMSuggest Changes

Decorators – Flexibly Extending Classes

With Decorators, JavaScript receives a powerful tool to specifically modify or enrich classes or their components without directly adapting the original code. The idea is to make metadata, logic, or changes to classes, methods, fields, or accessors by using a special function notation written directly above the declaration.

Some of you will already know these possibilities from TypeScript. With the implementation of ES2023 in common browsers, it will then also be usable as a direct standard in JavaScript.

Important Note: Decorators in JavaScript are currently still in the stage of a TC39 Proposal (Stage 3). This means they are not yet fully standardized and are only available in some environments with special flags or transpilers (e.g., Babel, TypeScript). Nevertheless, it is worth taking a look to understand how they are intended.

What are Decorators?

Decorators are functions used to modify class or member definitions. They are written directly before the class or a class element (like a method or a field) and can thus influence the definition, such as:

Simple Example: Class Decorator

A class decorator is a function that receives a class and either modifies it or returns a new class.

function sealed(constructor) {
  // Example decorator that prevents extending the class after definition
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Example {
  constructor(value) {
    this.value = value;
  }
}

const ex = new Example(10);
console.log(ex.value); // 10

// Attempt to modify the class afterwards fails
try {
  Example.prototype.newProp = 'test';
} catch (err) {
  console.error('Class is sealed:', err.message);
}

Here, the Example class is modified by the @sealed decorator. The decorator ensures that neither the class nor its prototype can be changed after declaration.

Decorating Methods

Often you want to provide individual methods with additional logic, such as logging calls or checking access rights. Decorators for methods receive information about the target object, the name of the method, and its original descriptors and can customize them.

function logCall(_, context) {
  const originalMethod = context.descriptor.value;
  context.descriptor.value = function(...args) {
    console.log(`Calling ${String(context.name)} with arguments:`, args);
    return originalMethod.apply(this, args);
  };
}

class Calculator {
  @logCall
  add(a, b) {
    return a + b;
  }
}

const calc = new Calculator();
console.log(calc.add(3, 7)); 
// Output:
// Calling add with arguments: [ 3, 7 ]
// 10

In this example, logging is automatically executed when calling add before the original logic of the method is called. The decorator simply replaces the method with a new function that performs additional tasks.

Practical Scenarios for Decorators

1. Logging and Debugging:

function logExecution(_, context) {
  const originalMethod = context.descriptor.value;
  context.descriptor.value = function(...args) {
    const start = performance.now();
    console.log(`[LOG] Method "${String(context.name)}" called at: ${new Date().toISOString()}`);
    const result = originalMethod.apply(this, args);
    const end = performance.now();
    console.log(`[LOG] Execution of "${String(context.name)}" took: ${end - start} ms`);
    return result;
  };
}

class DataService {
  @logExecution
  fetchData() {
    // Simulated expensive process
    for (let i = 0; i < 1_000_000; i++) {}
    return { data: 'Example' };
  }
}

const service = new DataService();
service.fetchData();

2. Validation and Security:

function requireRole(requiredRole) {
  return function(_, context) {
    const originalMethod = context.descriptor.value;
    context.descriptor.value = function(user, ...args) {
      if (!user || user.role !== requiredRole) {
        throw new Error(`Access denied: User does not have required role "${requiredRole}".`);
      }

      if (typeof args[0] !== 'number') {
        throw new TypeError('First argument must be a number.');
      }

      return originalMethod.apply(this, [user, ...args]);
    };
  };
}

class SecureService {
  @requireRole('admin')
  deleteRecord(user, recordId) {
    console.log(`Record ${recordId} is being deleted by ${user.name}.`);
  }
}

const userAdmin = { name: 'Anna', role: 'admin' };
const secure = new SecureService();
secure.deleteRecord(userAdmin, 42); // Success

3. Caching and Memoization:

function memoize(_, context) {
  const originalMethod = context.descriptor.value;
  const cache = new Map();

  context.descriptor.value = function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log(`Cache hit for arguments: ${key}`);
      return cache.get(key);
    }
    const result = originalMethod.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

class MathService {
  @memoize
  expensiveCalculation(x, y) {
    for (let i = 0; i < 1_000_000; i++) {}
    return x * y;
  }
}

const math = new MathService();
console.log(math.expensiveCalculation(10, 20)); // Computes new
console.log(math.expensiveCalculation(10, 20)); // Returns from cache

Current Status

The Decorator syntax is not yet finalized. Currently, there are different proposals and implementations, for example in TypeScript and Babel. In TypeScript, decorators have been usable for a long time (with experimentalDecorators), so you can get a taste of what the final standard might look like. However, it is possible that the syntax may change slightly.

Conclusion

Decorators make classes and their components more modular, adaptable, and declarative. Instead of hard-coding logic into each method, for example, you can elegantly extend it through a decorator. This promotes clean, reusable code and is a big plus especially in larger projects or frameworks. Even though Decorators are not yet natively supported by all JavaScript engines, it is already worth getting to know the concept and being excited about upcoming language versions.

Experiment with Decorators in your next project and discover their potential!


Previous Post
Door 24
Next Post
Door 22