Sajiron
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.
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.
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.
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.
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.
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
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
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'
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" }
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();
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.
For more info: Decorators in TypeScript, tc39/proposal-decorators