JavaScript Decorators and Metadata Explained
JavaScript decorators and metadata represent a major evolution in the ECMAScript standard, providing developers with declarative tools to inspect, modify, and enhance classes and their members. This article explains the fundamentals of modern ECMAScript decorators, details how the associated Metadata API works, explores common use cases like dependency injection and validation, and clarifies how these features streamline metaprogramming in modern JavaScript.
What Are Decorators?
Decorators are functions applied to classes, methods, fields,
getters, setters, or accessors using the @ prefix syntax.
Instead of altering behavior at runtime via inheritance or manual
wrapping, decorators execute once when the class is defined, allowing
you to intercept, wrap, or modify the target construct.
In the standard ECMAScript specification (Stage 3), a decorator function receives two primary arguments:
- Value: The element being decorated (e.g., a method function, class definition, or undefined for fields).
- Context: An object containing metadata and helper utilities for the element being decorated.
The context object provides structured information,
including: - kind: The type of member being decorated
('class', 'method', 'getter',
'setter', 'field', or
'accessor'). - name: The name of the class or
member (as a string or symbol). - static: A boolean
indicating if the member is a static class member. -
private: A boolean indicating if the member is a private
class member (#member). - access: An object
containing get and set functions to access the
member. - addInitializer: A function enabling developers to
schedule logic to run during instance initialization.
How Decorators Work in Practice
Here is a basic example of a logging decorator in modern JavaScript:
function logged(value, context) {
const { kind, name } = context;
if (kind === "method") {
return function (...args) {
console.log(`Calling ${name} with arguments:`, args);
const result = value.apply(this, args);
console.log(`Result from ${name}:`, result);
return result;
};
}
}
class Calculator {
@logged
add(a, b) {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3);What is Decorator Metadata?
Historically, attaching metadata to classes required external
libraries such as reflect-metadata. The standard ECMAScript
Decorator Metadata proposal formalizes this capability directly into
JavaScript.
The context object includes a metadata
property, which is a plain JavaScript object shared across all
decorators applied to a given class and its members. Any properties
attached to context.metadata are aggregated and stored
under the well-known symbol Symbol.metadata on the class
constructor.
Example of Attaching and Reading Metadata
function tag(tagName) {
return function (value, context) {
context.metadata.tags = context.metadata.tags || [];
context.metadata.tags.push(tagName);
};
}
@tag("UserManagement")
class UserService {
@tag("Sensitive")
deleteUser(id) {
// deletion logic
}
}
// Accessing metadata via Symbol.metadata
const metadata = UserService[Symbol.metadata];
console.log(metadata.tags); // Output: ['Sensitive', 'UserManagement']Key Use Cases for Decorators and Metadata
- Dependency Injection: Frameworks can automatically register and resolve service dependencies by inspecting attached class metadata.
- Validation and Serialization: Field-level decorators can enforce constraints (such as type checks, minimum lengths, or format validation) or map API response models.
- Aspect-Oriented Programming (AOP): Repetitive concerns such as logging, authentication, rate limiting, and performance profiling can be applied cleanly across methods.
- State Management: Reactive frameworks can use
auto-accessor decorators (
accessor myField) to trigger automatic UI updates when class properties change.
Current Adoption
Modern decorators and metadata represent a standardized consensus that replaces legacy, non-standard implementations previously used in early versions of TypeScript and Babel. As the proposal reaches full finalization in the ECMAScript standard, native runtime support and modern toolchain compatibility continue to expand across Node.js, Deno, Bun, and browser engines.