JavaScript Reflect API: Metaprogramming
Metaprogramming is code that manipulates code—reading, modifying, or generating program structures at runtime. JavaScript has always supported metaprogramming through dynamic property access, `eval`,...
Key Insights
- The Reflect API provides functional equivalents to JavaScript’s object operators, returning predictable boolean success values instead of throwing exceptions, making metaprogramming more reliable and composable.
- Reflect methods are designed as perfect companions to Proxy traps, allowing you to invoke default behavior while layering custom logic—essential for building validation, observation, and interception patterns.
- Unlike Object static methods that often throw errors, Reflect methods return boolean success indicators, enabling cleaner error handling and making them better suited for programmatic property manipulation in frameworks and libraries.
Introduction to Metaprogramming and Reflect
Metaprogramming is code that manipulates code—reading, modifying, or generating program structures at runtime. JavaScript has always supported metaprogramming through dynamic property access, eval, and prototype manipulation, but these approaches were inconsistent and error-prone.
ES6 introduced the Reflect API to standardize metaprogramming operations. Every Reflect method corresponds to an internal operation that was previously only accessible through operators or scattered across different APIs. This consolidation provides a unified, functional interface for object manipulation.
Consider property access. Traditionally, you’d use bracket notation or the dot operator:
const user = { name: 'Alice', age: 30 };
// Traditional approach
const name = user['name'];
user['email'] = 'alice@example.com';
// Using Reflect
const reflectedName = Reflect.get(user, 'name');
Reflect.set(user, 'email', 'alice@example.com');
console.log(name === reflectedName); // true
At first glance, Reflect seems redundant. The power becomes apparent when you need programmatic, composable operations that handle edge cases consistently.
Core Reflect Methods for Property Manipulation
The fundamental Reflect methods mirror basic JavaScript operators but return boolean success values instead of throwing exceptions.
Reflect.get(target, propertyKey, receiver) retrieves property values, with optional receiver binding for getter functions:
const config = {
_apiKey: 'secret-key',
get apiKey() {
return this._apiKey;
}
};
// Standard retrieval
console.log(Reflect.get(config, 'apiKey')); // 'secret-key'
// With custom receiver
const proxy = { _apiKey: 'proxy-key' };
console.log(Reflect.get(config, 'apiKey', proxy)); // 'proxy-key'
Reflect.set(target, propertyKey, value, receiver) assigns values and returns a boolean indicating success:
const data = {};
if (Reflect.set(data, 'status', 'active')) {
console.log('Property set successfully');
}
// Handles frozen objects gracefully
Object.freeze(data);
const success = Reflect.set(data, 'status', 'inactive');
console.log(success); // false (no exception thrown)
Reflect.has() and Reflect.deleteProperty() provide functional equivalents to the in operator and delete keyword:
const inventory = { apples: 10, oranges: 5 };
// Check existence
if (Reflect.has(inventory, 'apples')) {
console.log('Apples in stock');
}
// Safe deletion with success check
const deleted = Reflect.deleteProperty(inventory, 'oranges');
if (deleted) {
console.log('Oranges removed from inventory');
}
// Attempting to delete non-configurable property
Object.defineProperty(inventory, 'apples', { configurable: false });
console.log(Reflect.deleteProperty(inventory, 'apples')); // false
This boolean return pattern eliminates try-catch blocks for expected failures, making code cleaner and more functional.
Function Invocation with Reflect.apply() and Reflect.construct()
Reflect.apply(target, thisArgument, argumentsList) provides a cleaner alternative to Function.prototype.apply():
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
const user = { name: 'Bob' };
// Traditional apply
const message1 = greet.apply(user, ['Hello', '!']);
// Reflect.apply
const message2 = Reflect.apply(greet, user, ['Hello', '!']);
console.log(message1 === message2); // true
// More powerful: apply without method borrowing
const max = Reflect.apply(Math.max, null, [5, 10, 3, 8]);
console.log(max); // 10
Reflect.construct(target, argumentsList, newTarget) instantiates objects with precise prototype control:
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
}
// Standard construction
const dog1 = new Dog('Rex', 'Labrador');
// Using Reflect.construct
const dog2 = Reflect.construct(Dog, ['Max', 'Beagle']);
// Advanced: construct with different prototype
const dog3 = Reflect.construct(Dog, ['Buddy', 'Poodle'], Animal);
console.log(dog3 instanceof Animal); // true
console.log(dog3 instanceof Dog); // false
This enables factory patterns where prototype chains are determined dynamically.
Reflect with Proxy for Interception Patterns
Reflect’s killer feature is its seamless integration with Proxy. Every Proxy trap has a corresponding Reflect method with identical signatures, making it trivial to invoke default behavior:
const validator = {
set(target, property, value) {
if (property === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
if (property === 'age' && value < 0) {
throw new RangeError('Age cannot be negative');
}
// Use Reflect to perform the actual set operation
return Reflect.set(target, property, value);
}
};
const person = new Proxy({}, validator);
person.name = 'Charlie'; // Works
person.age = 25; // Works
// person.age = -5; // Throws RangeError
// person.age = '30'; // Throws TypeError
Here’s an observable object pattern that tracks property changes:
function createObservable(target, onChange) {
return new Proxy(target, {
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
console.log(`Reading property: ${property}`);
return value;
},
set(target, property, value, receiver) {
const oldValue = target[property];
const success = Reflect.set(target, property, value, receiver);
if (success && oldValue !== value) {
onChange(property, oldValue, value);
}
return success;
}
});
}
const state = createObservable(
{ count: 0 },
(prop, oldVal, newVal) => {
console.log(`${prop} changed from ${oldVal} to ${newVal}`);
}
);
state.count = 5; // Logs: count changed from 0 to 5
Property Descriptors and Object Extension Control
Reflect provides methods for fine-grained property control that mirror Object methods but with consistent return values:
const obj = {};
// Define property with descriptor
const defined = Reflect.defineProperty(obj, 'id', {
value: 42,
writable: false,
enumerable: true,
configurable: false
});
console.log(defined); // true
// Get property descriptor
const descriptor = Reflect.getOwnPropertyDescriptor(obj, 'id');
console.log(descriptor.writable); // false
// Attempt to redefine non-configurable property
const redefined = Reflect.defineProperty(obj, 'id', {
value: 100
});
console.log(redefined); // false (Object.defineProperty would throw)
Build a sealed object wrapper utility:
function createSealedObject(initialData) {
const obj = { ...initialData };
// Prevent extensions
Reflect.preventExtensions(obj);
// Make all properties non-configurable
for (const key of Reflect.ownKeys(obj)) {
const descriptor = Reflect.getOwnPropertyDescriptor(obj, key);
Reflect.defineProperty(obj, key, {
...descriptor,
configurable: false
});
}
return obj;
}
const config = createSealedObject({ apiUrl: 'https://api.example.com' });
console.log(Reflect.isExtensible(config)); // false
config.apiUrl = 'https://new-api.example.com'; // Works (writable by default)
delete config.apiUrl; // Fails silently
config.newProperty = 'value'; // Fails silently
Practical Use Cases and Patterns
Reflect shines in framework code. Here’s a mini validation framework:
class Validator {
static rules = {
required: (value) => value !== undefined && value !== null && value !== '',
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
minLength: (min) => (value) => value.length >= min,
range: (min, max) => (value) => value >= min && value <= max
};
static create(schema) {
return new Proxy({}, {
set(target, property, value) {
const rules = schema[property];
if (rules) {
for (const [ruleName, ruleConfig] of Object.entries(rules)) {
const validator = typeof ruleConfig === 'function'
? ruleConfig
: Validator.rules[ruleName](ruleConfig);
if (!validator(value)) {
throw new Error(
`Validation failed for ${property}: ${ruleName}`
);
}
}
}
return Reflect.set(target, property, value);
},
get(target, property) {
return Reflect.get(target, property);
}
});
}
}
const userSchema = {
email: { required: true, email: true },
age: { required: true, range: [0, 120] },
username: { required: true, minLength: 3 }
};
const user = Validator.create(userSchema);
user.email = 'user@example.com'; // Valid
user.age = 25; // Valid
// user.email = 'invalid'; // Throws validation error
// user.age = 150; // Throws validation error
Reflect vs. Object Methods: When to Use Which
Many Reflect methods have Object counterparts, but with crucial differences:
const obj = {};
// Object.defineProperty throws on failure
try {
Object.freeze(obj);
Object.defineProperty(obj, 'prop', { value: 42 });
console.log('Success');
} catch (e) {
console.log('Object.defineProperty threw:', e.message);
}
// Reflect.defineProperty returns boolean
Object.freeze(obj);
const success = Reflect.defineProperty(obj, 'prop', { value: 42 });
if (!success) {
console.log('Reflect.defineProperty returned false');
}
Use Reflect when:
- Building libraries or frameworks that need predictable, composable operations
- Working with Proxy handlers
- You want functional programming patterns with boolean returns
- Error handling should be explicit rather than exception-based
Use Object methods when:
- You want exceptions for error conditions (fail-fast behavior)
- Writing application code where failures are truly exceptional
- Working with legacy code that expects exceptions
The Reflect API transforms JavaScript metaprogramming from an ad-hoc collection of operators and methods into a coherent, functional toolkit. For framework authors and library developers, it’s indispensable. For application developers, it provides cleaner patterns for dynamic property manipulation and validation. Master Reflect, and you’ll write more robust, maintainable metaprogramming code.