JS Metaprogramming Advanced Architecture Guide
Master JavaScript metaprogramming for dynamic, self-aware code architectures. Covers Proxy meta-object protocol, Reflect API integration, Symbol-based protocols, property descriptors, dynamic class creation, aspect-oriented programming, DSL construction, and compile-time vs runtime metaprogramming.
Metaprogramming is code that manipulates code. JavaScript provides three pillars for metaprogramming: Proxy (intercept operations), Reflect (perform operations programmatically), and Symbol (define protocols). Together they enable dynamic dispatch, aspect-oriented programming, domain-specific languages, and self-modifying architectures.
For Proxy and Reflect fundamentals, see Using Reflect and Proxy Together in JavaScript.
The Meta-Object Protocol
// JavaScript's MOP exposes 13 fundamental operations via Proxy traps
// These intercept ALL object interactions at the language level
// TRANSPARENT PROXY: intercepts everything, logs operations
function createMetaProxy(target, label = "object") {
const traps = [
"get", "set", "has", "deleteProperty", "ownKeys",
"getOwnPropertyDescriptor", "defineProperty",
"getPrototypeOf", "setPrototypeOf", "isExtensible",
"preventExtensions", "apply", "construct"
];
const handler = {};
for (const trap of traps) {
handler[trap] = function (...args) {
console.log(`[${label}] ${trap}(${formatArgs(args.slice(1))})`);
return Reflect[trap](...args);
};
}
return new Proxy(target, handler);
}
function formatArgs(args) {
return args.map(a => {
if (typeof a === "symbol") return a.toString();
if (typeof a === "object" && a !== null) return JSON.stringify(a);
return String(a);
}).join(", ");
}
const obj = createMetaProxy({ x: 1, y: 2 }, "point");
obj.x; // [point] get(x)
obj.z = 3; // [point] set(z, 3)
"x" in obj; // [point] has(x)
delete obj.z; // [point] deleteProperty(z)
Object.keys(obj); // [point] ownKeys()
// VIRTUAL OBJECTS: no real properties, everything is computed
function createVirtualObject(computeFn) {
return new Proxy(Object.create(null), {
get(target, prop) {
if (prop === Symbol.toPrimitive) return () => "[VirtualObject]";
if (typeof prop === "symbol") return undefined;
return computeFn(prop);
},
has(target, prop) {
return true; // Every property "exists"
},
ownKeys() {
return []; // No enumerable properties
},
getOwnPropertyDescriptor(target, prop) {
return {
value: computeFn(prop),
writable: false,
enumerable: true,
configurable: true
};
}
});
}
// Math constants virtual object
const constants = createVirtualObject(name => {
const values = { PI: Math.PI, E: Math.E, TAU: Math.PI * 2, PHI: (1 + Math.sqrt(5)) / 2 };
return values[name] || undefined;
});
console.log(constants.PI); // 3.141592653589793
console.log(constants.TAU); // 6.283185307179586
console.log(constants.PHI); // 1.618033988749895Symbol-Based Protocols
// Well-known Symbols define object behavior in the language
// Symbol.iterator: make objects iterable
// Symbol.toPrimitive: control type conversion
// Symbol.hasInstance: customize instanceof
// Symbol.species: control constructor for derived objects
// Symbol.toStringTag: customize Object.prototype.toString
class Money {
#amount;
#currency;
constructor(amount, currency = "USD") {
this.#amount = amount;
this.#currency = currency;
}
// Control type coercion
[Symbol.toPrimitive](hint) {
switch (hint) {
case "number": return this.#amount;
case "string": return `${this.#currency} ${this.#amount.toFixed(2)}`;
default: return this.#amount;
}
}
// Customize Object.prototype.toString
get [Symbol.toStringTag]() {
return "Money";
}
// Customize instanceof
static [Symbol.hasInstance](instance) {
return instance &&
typeof instance[Symbol.toPrimitive] === "function" &&
typeof instance.convert === "function";
}
// Make iterable (yields [currency, amount])
*[Symbol.iterator]() {
yield this.#currency;
yield this.#amount;
}
convert(targetCurrency, rate) {
return new Money(this.#amount * rate, targetCurrency);
}
}
const price = new Money(29.99, "USD");
console.log(+price); // 29.99 (number hint)
console.log(`${price}`); // "USD 29.99" (string hint)
console.log(price + 10); // 39.99 (default hint -> number)
console.log(Object.prototype.toString.call(price)); // "[object Money]"
const [currency, amount] = price;
console.log(currency, amount); // "USD" 29.99
// CUSTOM PROTOCOL WITH SYMBOLS
const Serializable = Symbol("Serializable");
const Deserializable = Symbol("Deserializable");
class Model {
[Serializable]() {
const data = {};
for (const key of Object.keys(this)) {
data[key] = this[key];
}
return JSON.stringify(data);
}
static [Deserializable](json) {
const data = JSON.parse(json);
const instance = new this();
Object.assign(instance, data);
return instance;
}
}
function serialize(obj) {
if (typeof obj[Serializable] !== "function") {
throw new TypeError("Object is not serializable");
}
return obj[Serializable]();
}
function deserialize(Class, json) {
if (typeof Class[Deserializable] !== "function") {
throw new TypeError("Class is not deserializable");
}
return Class[Deserializable](json);
}Dynamic Class Construction
// Create classes at runtime based on schemas or configurations
function createClass(name, schema) {
const { properties = {}, methods = {}, validators = {} } = schema;
// Build the class dynamically
const DynamicClass = class {
constructor(data = {}) {
// Validate and assign properties
for (const [prop, config] of Object.entries(properties)) {
const value = data[prop] !== undefined ? data[prop] : config.default;
if (config.required && value === undefined) {
throw new Error(`${name}: ${prop} is required`);
}
if (value !== undefined && config.type) {
const actualType = typeof value;
if (actualType !== config.type) {
throw new TypeError(
`${name}.${prop}: expected ${config.type}, got ${actualType}`
);
}
}
if (validators[prop] && value !== undefined) {
const error = validators[prop](value);
if (error) throw new Error(`${name}.${prop}: ${error}`);
}
this[prop] = value;
}
}
toJSON() {
const result = {};
for (const prop of Object.keys(properties)) {
if (this[prop] !== undefined) result[prop] = this[prop];
}
return result;
}
get [Symbol.toStringTag]() {
return name;
}
};
// Add methods
for (const [methodName, fn] of Object.entries(methods)) {
DynamicClass.prototype[methodName] = fn;
}
// Set the class name
Object.defineProperty(DynamicClass, "name", { value: name });
return DynamicClass;
}
const UserModel = createClass("User", {
properties: {
name: { type: "string", required: true },
email: { type: "string", required: true },
age: { type: "number", default: 0 },
role: { type: "string", default: "user" }
},
validators: {
email: (v) => v.includes("@") ? null : "invalid email format",
age: (v) => v >= 0 ? null : "age must be non-negative"
},
methods: {
greet() { return `Hello, I'm ${this.name}`; },
isAdmin() { return this.role === "admin"; }
}
});
const user = new UserModel({ name: "Alice", email: "alice@example.com", age: 30 });
console.log(user.greet()); // "Hello, I'm Alice"
console.log(user.toJSON()); // { name: "Alice", email: "alice@example.com", age: 30, role: "user" }
console.log(UserModel.name); // "User"
// MIXIN COMPOSITION
function mixin(Base, ...mixins) {
class Mixed extends Base {}
for (const mix of mixins) {
// Copy instance methods
for (const key of Object.getOwnPropertyNames(mix.prototype)) {
if (key === "constructor") continue;
Object.defineProperty(
Mixed.prototype,
key,
Object.getOwnPropertyDescriptor(mix.prototype, key)
);
}
// Copy static methods
for (const key of Object.getOwnPropertyNames(mix)) {
if (["prototype", "name", "length"].includes(key)) continue;
Object.defineProperty(
Mixed,
key,
Object.getOwnPropertyDescriptor(mix, key)
);
}
}
return Mixed;
}
class Timestamped {
initTimestamps() {
this.createdAt = new Date();
this.updatedAt = new Date();
}
touch() {
this.updatedAt = new Date();
}
}
class SoftDeletable {
softDelete() {
this.deletedAt = new Date();
this.isDeleted = true;
}
restore() {
this.deletedAt = null;
this.isDeleted = false;
}
}
const EnhancedUser = mixin(UserModel, Timestamped, SoftDeletable);
const enhanced = new EnhancedUser({ name: "Bob", email: "bob@example.com" });
enhanced.initTimestamps();
enhanced.softDelete();
console.log(enhanced.isDeleted); // true
enhanced.restore();
console.log(enhanced.isDeleted); // falseAspect-Oriented Programming
// AOP: add cross-cutting concerns (logging, caching, auth) without modifying code
class Aspect {
static before(target, methodName, advice) {
const original = target.prototype[methodName];
target.prototype[methodName] = function (...args) {
advice.call(this, methodName, args);
return original.apply(this, args);
};
}
static after(target, methodName, advice) {
const original = target.prototype[methodName];
target.prototype[methodName] = function (...args) {
const result = original.apply(this, args);
advice.call(this, methodName, result, args);
return result;
};
}
static around(target, methodName, advice) {
const original = target.prototype[methodName];
target.prototype[methodName] = function (...args) {
return advice.call(this, original.bind(this), args);
};
}
// Apply aspects via Proxy (non-invasive)
static wrap(instance, aspects) {
return new Proxy(instance, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value !== "function") return value;
return function (...args) {
const ctx = { target, method: prop, args };
// Before advice
for (const aspect of aspects) {
if (aspect.before) aspect.before(ctx);
}
let result;
try {
result = value.apply(target, args);
ctx.result = result;
// After advice (on success)
for (const aspect of aspects) {
if (aspect.afterReturning) aspect.afterReturning(ctx);
}
} catch (err) {
ctx.error = err;
// After throwing advice
for (const aspect of aspects) {
if (aspect.afterThrowing) aspect.afterThrowing(ctx);
}
throw err;
} finally {
// After (always) advice
for (const aspect of aspects) {
if (aspect.after) aspect.after(ctx);
}
}
return result;
};
}
});
}
}
// Usage
class OrderService {
placeOrder(item, quantity) {
return { id: Date.now(), item, quantity, total: quantity * 9.99 };
}
cancelOrder(orderId) {
return { id: orderId, status: "cancelled" };
}
}
const loggingAspect = {
before(ctx) {
console.log(`Calling ${ctx.method}(${JSON.stringify(ctx.args)})`);
},
afterReturning(ctx) {
console.log(`${ctx.method} returned: ${JSON.stringify(ctx.result)}`);
},
afterThrowing(ctx) {
console.error(`${ctx.method} threw: ${ctx.error.message}`);
}
};
const timingAspect = {
before(ctx) {
ctx.startTime = performance.now();
},
after(ctx) {
const duration = performance.now() - ctx.startTime;
console.log(`${ctx.method} took ${duration.toFixed(2)}ms`);
}
};
const service = Aspect.wrap(new OrderService(), [loggingAspect, timingAspect]);
service.placeOrder("Widget", 3);
// Calling placeOrder(["Widget",3])
// placeOrder returned: {"id":...,"item":"Widget","quantity":3,"total":29.97}
// placeOrder took 0.12msDomain-Specific Languages
// Build DSLs using Proxy for fluent, readable APIs
function createQueryBuilder() {
const query = {
table: null,
conditions: [],
fields: ["*"],
orderBy: null,
limitVal: null,
joins: []
};
const builder = new Proxy({}, {
get(target, prop) {
switch (prop) {
case "from":
return (table) => { query.table = table; return builder; };
case "select":
return (...fields) => { query.fields = fields; return builder; };
case "where":
return (field, op, value) => {
query.conditions.push({ field, op, value });
return builder;
};
case "and":
return builder; // Syntactic sugar, returns same builder
case "orderBy":
return (field, dir = "ASC") => {
query.orderBy = { field, dir };
return builder;
};
case "limit":
return (n) => { query.limitVal = n; return builder; };
case "join":
return (table, on) => {
query.joins.push({ table, on });
return builder;
};
case "build":
return () => buildSQL(query);
case "toJSON":
return () => ({ ...query });
default:
return undefined;
}
}
});
return builder;
}
function buildSQL(q) {
let sql = `SELECT ${q.fields.join(", ")} FROM ${q.table}`;
for (const j of q.joins) {
sql += ` JOIN ${j.table} ON ${j.on}`;
}
if (q.conditions.length > 0) {
const where = q.conditions.map(c =>
`${c.field} ${c.op} '${c.value}'`
).join(" AND ");
sql += ` WHERE ${where}`;
}
if (q.orderBy) {
sql += ` ORDER BY ${q.orderBy.field} ${q.orderBy.dir}`;
}
if (q.limitVal) {
sql += ` LIMIT ${q.limitVal}`;
}
return sql;
}
const sql = createQueryBuilder()
.from("users")
.select("id", "name", "email")
.where("age", ">", 18)
.and.where("status", "=", "active")
.join("orders", "orders.user_id = users.id")
.orderBy("name")
.limit(10)
.build();
console.log(sql);
// SELECT id, name, email FROM users JOIN orders ON orders.user_id = users.id
// WHERE age > '18' AND status = 'active' ORDER BY name ASC LIMIT 10
// VALIDATION DSL
function schema() {
const rules = [];
const builder = new Proxy({}, {
get(_, prop) {
switch (prop) {
case "string":
rules.push({ type: "string" });
return builder;
case "number":
rules.push({ type: "number" });
return builder;
case "required":
rules.push({ check: v => v != null, msg: "is required" });
return builder;
case "min":
return (n) => { rules.push({ check: v => v >= n, msg: `must be >= ${n}` }); return builder; };
case "max":
return (n) => { rules.push({ check: v => v <= n, msg: `must be <= ${n}` }); return builder; };
case "matches":
return (re) => { rules.push({ check: v => re.test(v), msg: `must match ${re}` }); return builder; };
case "validate":
return (value) => {
const errors = [];
for (const rule of rules) {
if (rule.type && typeof value !== rule.type) {
errors.push(`expected ${rule.type}`);
}
if (rule.check && !rule.check(value)) {
errors.push(rule.msg);
}
}
return errors.length > 0 ? errors : null;
};
default:
return builder;
}
}
});
return builder;
}
const ageValidator = schema().number.required.min(0).max(150);
console.log(ageValidator.validate(25)); // null (valid)
console.log(ageValidator.validate(-5)); // ["must be >= 0"]
console.log(ageValidator.validate(200)); // ["must be <= 150"]| Technique | Mechanism | Use Case | Trade-off |
|---|---|---|---|
| Proxy traps | Intercept operations | Virtual objects, logging, access control | Runtime overhead per operation |
| Reflect API | Programmatic operations | Forward operations, maintain invariants | Slight verbosity vs direct access |
| Well-known Symbols | Protocol hooks | Iterables, type coercion, instanceof | Must understand protocol contracts |
| Dynamic classes | Runtime construction | Schema-driven models, ORMs | Harder to type-check statically |
| AOP via Proxy | Non-invasive wrapping | Logging, caching, auth, timing | Added indirection layer |
| DSL builders | Proxy get traps | Query builders, validation, routing | Limited IDE autocomplete |
Rune AI
Key Insights
- The meta-object protocol (13 Proxy traps + Reflect) intercepts ALL fundamental operations on objects: This enables virtual objects, transparent logging, access control, and invariant enforcement
- Well-known Symbols define how objects participate in language protocols like iteration, type coercion, and instanceof: Custom Symbol-based protocols extend this pattern for application-level contracts
- Dynamic class construction from schemas enables runtime model generation for ORMs, form builders, and configuration-driven architectures: Combine with mixin composition for reusable cross-cutting behaviors
- Aspect-oriented programming via Proxy wrapping adds logging, caching, timing, and authorization without modifying original code: The Proxy-based approach is non-invasive and reversible
- DSL builders using Proxy get traps create fluent, readable APIs for query construction, validation rules, and configuration: Each property access returns the builder for method chaining
Frequently Asked Questions
Does metaprogramming hurt performance?
How do I combine multiple metaprogramming techniques safely?
Can TypeScript understand metaprogrammed code?
When is metaprogramming the wrong choice?
Conclusion
JavaScript metaprogramming through Proxy, Reflect, and Symbols enables dynamic object behavior, aspect-oriented design, and fluent DSLs. These tools are most valuable at framework boundaries where they simplify consumer-facing APIs. For self-modifying code patterns that extend these concepts, see Writing Self-Modifying Code in JS Architecture. For the Proxy-Reflect integration patterns underlying these techniques, revisit Using Reflect and Proxy Together in JavaScript.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.