S

Sajiron

21 min readPublished on Feb 03, 2025

JavaScript Decorators & Annotations: A Practical Guide to Metaprogramming

DALL·E 2025-02-03 16.17.14 - A visually appealing infographic illustrating the differences between JavaScript decorators and Java annotations. The image should have a modern tech-.webp

Metaprogramming in JavaScript is an exciting technique that lets you dynamically modify, inspect, or even generate code. If you've used annotations in Java, you might wonder if JavaScript has an equivalent. While JavaScript doesn't have built-in annotations, it does provide decorators, which serve a similar purpose.

Decorators are still a Stage 3 ECMAScript proposal, but they are widely adopted in TypeScript and Babel for class-based programming. Additionally, JavaScript provides the Reflect API, which enables metadata reflection and dynamic property access, making decorators even more powerful.

This guide will break down decorators, the Reflect API, and real-world examples in a way that's easy to follow and practical for your projects.

📌 Annotations vs. Decorators

Both annotations and decorators provide metadata and influence how code behaves, but they differ in their implementation and functionality.

Annotations

Used in languages like Java, Python, and C#.

Provide metadata but do not modify behavior directly.

Typically processed at compile-time.

Examples: @Override, @Deprecated, @Entity in Java.

Mainly used for frameworks, ORM mappings, dependency injection.

Decorators

Used in JavaScript (TypeScript, Babel).

Modify class, method, or property behavior dynamically.

Executed at runtime rather than compile-time.

Can intercept, enhance, or replace functionality.

Examples: @Component, @Injectable, @readonly.

JavaScript lacks native annotations, but decorators provide similar functionality with additional runtime flexibility.

📌 What Are Annotations?

Annotations in Java act as metadata markers that provide extra information about a class, method, or property. They do not change the behavior of the program but serve as metadata that can be processed at runtime or compile time.

Key Characteristics of Annotations:

Used for metadata (e.g., @Override, @Deprecated).

Typically processed at compile time.

Helps frameworks and tools understand code structure.

JavaScript doesn’t support annotations natively, but we can achieve similar functionality using decorators.

🛠️ What Are JavaScript Decorators?

Decorators are a powerful metaprogramming tool that allows modifying classes, methods, and properties at runtime. They enable reusable and declarative modifications to class-based structures.

Key Features of Decorators:

Work at runtime.

Modify class behaviors without modifying the original code.

Used in frameworks like Angular and NestJS.

Implemented in TypeScript and Babel but not yet officially part of ECMAScript.

🔧 Enabling Decorators in TypeScript

To use decorators in TypeScript, enable them in your tsconfig.json:

{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

Now, let's dive into some practical examples of decorators.

1. Method Decorators: Logging Function Calls

A method decorator modifies a function's behavior by wrapping it with additional logic. This example logs function calls:

function log(target: any, key: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const originalMethod = descriptor.value;

descriptor.value = function (...args: any[]) {
console.log(`Calling ${key} with args:`, args);

return originalMethod.apply(this, args);
};

return descriptor;
}

class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}

const calc = new Calculator();
console.log(calc.add(2, 3));
// Logs: Calling add with args: [2, 3]
// Output: 5

2. Class Decorators: Sealing a Class

Class decorators modify an entire class. A sealed class cannot have new properties added after instantiation:

'use strict';

function sealed(constructor: T): T {
Object.seal(constructor);
Object.seal(constructor.prototype);

return class extends constructor {
constructor(...args: any[]) {
super(...args);
Object.seal(this);
}
};
}

@sealed
class Person {
name: string;

constructor(name: string) {
this.name = name;
}
}

const p = new Person('John');
p.age = 30; // TypeError: Cannot add property age, object is not extensible

3. Property Decorators: Making Properties Read-Only

A property decorator controls how a property behaves. The example below makes a property immutable:

function readonly(target: any, key: string) {
const privateKey = `_${key}`; // Internal key to store value

Object.defineProperty(target, key, {
get: function () {
return this[privateKey];
},
set: function (value: any) {
if (this[privateKey] === undefined) {
this[privateKey] = value;

return;
}

throw new Error(`Cannot reassign readonly property '${key}'`);
},
enumerable: true,
configurable: false,
});
}

class User {
@readonly
name = 'John';
}

const user = new User();
console.log(user.name); // John

user.name = 'Alice'; // Error: Cannot reassign readonly property 'name'

4. Using Reflect API for Custom Metadata

The Reflect API provides methods for dynamically interacting with objects, modifying class behaviors, and handling metadata. It enables:

Accessing and modifying object properties.

Calling constructors dynamically.

Checking or defining new properties.

Applying functions dynamically.

However, the Reflect API does not support metadata reflection natively. If you need to work with metadata in decorators, you must install the reflect-metadata module.

To enable metadata reflection in your project, install reflect-metadata:

npm install reflect-metadata

Then, import it at the top of your TypeScript file:

import "reflect-metadata";

Using reflect-metadata with Decorators

import 'reflect-metadata';

function annotate(metaData: any) {
return (target: any, key: string) => {
Reflect.defineMetadata(key, metaData, target, key);
};
}

class User {
@annotate({ role: 'admin' })
role!: string;
}

const meta = Reflect.getMetadata('role', User.prototype, 'role');
console.log(meta); // { role: "admin" }

5. Automating API Calls with Decorators

A decorator can be used to fetch data automatically when calling a method:

function fetchData(url: string) {
return (target: any, key: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
const originalMethod = descriptor.value;

descriptor.value = async function (...args: any[]) {
const response = await fetch(url);
const data = await response.json();

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

return descriptor;
};
}

class APIClient {
@fetchData('https://jsonplaceholder.typicode.com/users')
printUsers(data = null) {
console.log('Users:', data);
}
}

const client = new APIClient();
client.printUsers();

🎯 Conclusion

Both annotations and decorators play a crucial role in software development by providing metadata and modifying behavior where needed. While JavaScript does not natively support annotations like Java, decorators offer a more dynamic, runtime-based approach to enhancing class behavior.

By understanding the differences and use cases, you can effectively leverage decorators in JavaScript to build scalable, maintainable, and flexible applications. Whether you're working with TypeScript, Babel, or modern JavaScript frameworks, decorators provide a powerful mechanism for code reusability and abstraction.

As the JavaScript ecosystem evolves, we may see decorators become officially part of ECMAScript, bringing even greater capabilities to the language.