Metaprogramming means writing code that changes or mediates language behavior itself. It is powerful, but expensive in complexity and runtime predictability.
This module focuses on production-grade reasoning, not toy metaprogramming tricks.
Metaprogramming is useful when you need:
- observability: logging, auditing, dependency tracking
- control: capability restrictions, virtualized objects, policy enforcement
- interoperability: custom coercion and protocol integration (iterables, instances, tags)
- Observability wrappers should be as transparent as possible.
- Control wrappers intentionally alter behavior (e.g., deny writes, mask fields).
- Mixing both often produces fragile APIs.
- Stack traces become harder to read.
- Proxy behavior is implicit and can surprise maintainers.
- Tooling and type systems have weaker visibility into dynamic traps.
Proxy usage can push code onto slower engine paths and reduce optimization opportunities. Even "small" proxy wrappers can affect hot paths significantly.
Proxy/Symbol metaprogramming is a power tool:
- useful for framework internals, sandboxing, virtualization
- often overkill for standard business objects
'use strict';
const target = { x: 1 };
const handler = {
get(t, prop, receiver) {
return Reflect.get(t, prop, receiver);
},
};
const proxy = new Proxy(target, handler);- target: underlying object/function
- handler: trap object that customizes operations
getsethasownKeysgetOwnPropertyDescriptordefinePropertydeleteProperty
applyfor calls:proxy(...)constructfor construction:new proxy(...)
getPrototypeOfsetPrototypeOf
'use strict';
const { proxy, revoke } = Proxy.revocable({ secret: 1 }, {});
console.log(proxy.secret); // 1
revoke();
// Any further interaction throws TypeError.The engine enforces invariants and throws when traps violate them.
'use strict';
const target = {};
Object.defineProperty(target, 'fixed', {
value: 1,
configurable: false,
enumerable: true,
});
const proxy = new Proxy(target, {
ownKeys() {
return []; // illegal: must include non-configurable own keys
},
});
Reflect.ownKeys(proxy); // TypeError'use strict';
const target = {};
Object.defineProperty(target, 'fixed', {
value: 1,
configurable: false,
enumerable: true,
});
const proxy = new Proxy(target, {
has() {
return false; // illegal for non-configurable own property
},
});
'fixed' in proxy; // TypeErrorIf target has a non-configurable property, trap cannot report incompatible descriptor or "missing" descriptor.
- Transparent proxy: preserves behavior, adds diagnostics/metrics.
- Non-transparent proxy: intentionally alters visibility/semantics.
Use transparent defaults unless policy requires non-transparency.
Reflect is the canonical companion for trap forwarding and invariant-safe behavior.
Key methods:
Reflect.getReflect.setReflect.applyReflect.constructReflect.definePropertyReflect.getOwnPropertyDescriptorReflect.ownKeys
Forwarding with direct operations is often subtly wrong.
'use strict';
const handler = {
get(target, prop, receiver) {
// Correct: preserves receiver semantics for getters.
return Reflect.get(target, prop, receiver);
},
};For function proxies:
apply(target, thisArg, args) {
return Reflect.apply(target, thisArg, args);
}
construct(target, args, newTarget) {
return Reflect.construct(target, args, newTarget);
}'use strict';
const a = Symbol('id');
const b = Symbol('id');
console.log(a === b); // false'use strict';
const s1 = Symbol.for('app.user');
const s2 = Symbol.for('app.user');
console.log(s1 === s2); // true
console.log(Symbol.keyFor(s1)); // 'app.user'Symbol.iteratorSymbol.asyncIteratorSymbol.toStringTagSymbol.toPrimitiveSymbol.hasInstanceSymbol.species(constructor selection for derived operations)Symbol.isConcatSpreadable(array concat spreading behavior)
'use strict';
const secret = Symbol('secret');
const obj = { visible: 1, [secret]: 2 };
Object.keys(obj); // ['visible']
Reflect.ownKeys(obj); // ['visible', Symbol(secret)]
Object.getOwnPropertySymbols(obj); // [Symbol(secret)]Symbol.toPrimitive customizes coercion.
Hints:
'number''string''default'
'use strict';
const x = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') return 10;
if (hint === 'string') return '#10';
return 10; // default
},
};Number(x)-> hint'number'String(x)and template literals -> hint'string'- binary
+with object -> usually hint'default'
If Symbol.toPrimitive exists, it is consulted before valueOf/toString.
'use strict';
const x = {
[Symbol.toPrimitive](hint) {
if (hint === 'string') return '#7';
return 7;
},
};
x + 1; // 8 (default hint numeric here)
`${x}`; // '#7' (string hint)'use strict';
const x = {
[Symbol.toPrimitive](hint) {
return hint === 'number' ? 2 : '2';
},
};
Number(x); // 2
x == 2; // true (default coercion path)
String(x); // '2'If default returns string in arithmetic-heavy code, + may concatenate instead of add.
Say this directly:
- Traps can customize operations.
- But invariants are enforced by engine.
- Non-configurable and non-extensible constraints cannot be violated.
- Reflect is safest for forwarding to preserve semantics.
Coercion order:
- Check
Symbol.toPrimitive. - If absent, fallback to ordinary conversion (
valueOf/toStringorder depends on hint). - Ensure returned primitive matches operation intent.
Acceptable:
- infrastructure boundaries
- controlled API virtualization
- instrumentation wrappers with tests
Code smell:
- replacing straightforward domain modeling
- hiding business rules behind traps
- performance-critical loops with broad proxy mediation
Use metaprogramming when the protocol-level gain justifies complexity.