Proxy handlers intercept operations on objects, enabling powerful meta-programming patterns. Here's how to use each trap effectively.
Basic Structure#
1const target = { name: 'Original' };
2
3const handler = {
4 get(target, property, receiver) {
5 console.log(`Getting ${property}`);
6 return Reflect.get(target, property, receiver);
7 },
8
9 set(target, property, value, receiver) {
10 console.log(`Setting ${property} to ${value}`);
11 return Reflect.set(target, property, value, receiver);
12 },
13};
14
15const proxy = new Proxy(target, handler);
16
17proxy.name; // "Getting name"
18proxy.age = 25; // "Setting age to 25"get Trap#
1const handler = {
2 get(target, property, receiver) {
3 // Property doesn't exist
4 if (!(property in target)) {
5 return `Property '${property}' not found`;
6 }
7
8 // Transform value
9 const value = Reflect.get(target, property, receiver);
10 if (typeof value === 'string') {
11 return value.toUpperCase();
12 }
13
14 return value;
15 },
16};
17
18const obj = new Proxy({ name: 'john' }, handler);
19console.log(obj.name); // 'JOHN'
20console.log(obj.missing); // "Property 'missing' not found"set Trap#
1const handler = {
2 set(target, property, value, receiver) {
3 // Validation
4 if (property === 'age' && (typeof value !== 'number' || value < 0)) {
5 throw new TypeError('Age must be a positive number');
6 }
7
8 // Transformation
9 if (property === 'name' && typeof value === 'string') {
10 value = value.trim();
11 }
12
13 // Logging
14 console.log(`${property} changed to ${value}`);
15
16 return Reflect.set(target, property, value, receiver);
17 },
18};
19
20const user = new Proxy({}, handler);
21user.name = ' John '; // Trimmed to 'John'
22user.age = 30; // OK
23// user.age = -5; // TypeErrorhas Trap#
1const handler = {
2 has(target, property) {
3 // Hide private properties
4 if (property.startsWith('_')) {
5 return false;
6 }
7
8 return Reflect.has(target, property);
9 },
10};
11
12const obj = new Proxy({ name: 'John', _secret: 'hidden' }, handler);
13console.log('name' in obj); // true
14console.log('_secret' in obj); // false (hidden)deleteProperty Trap#
1const handler = {
2 deleteProperty(target, property) {
3 // Prevent deletion of certain properties
4 if (property === 'id') {
5 console.log('Cannot delete id');
6 return false;
7 }
8
9 // Log deletion
10 console.log(`Deleting ${property}`);
11
12 return Reflect.deleteProperty(target, property);
13 },
14};
15
16const obj = new Proxy({ id: 1, name: 'John' }, handler);
17delete obj.name; // "Deleting name" - succeeds
18delete obj.id; // "Cannot delete id" - failsownKeys Trap#
1const handler = {
2 ownKeys(target) {
3 // Filter out private properties
4 return Reflect.ownKeys(target).filter(
5 (key) => !String(key).startsWith('_')
6 );
7 },
8};
9
10const obj = new Proxy(
11 { name: 'John', age: 30, _private: 'hidden' },
12 handler
13);
14
15console.log(Object.keys(obj)); // ['name', 'age']
16console.log(Object.entries(obj)); // [['name', 'John'], ['age', 30]]apply Trap (Functions)#
1const handler = {
2 apply(target, thisArg, argumentsList) {
3 console.log(`Calling function with args: ${argumentsList}`);
4
5 // Modify arguments
6 const modifiedArgs = argumentsList.map((arg) =>
7 typeof arg === 'number' ? arg * 2 : arg
8 );
9
10 return Reflect.apply(target, thisArg, modifiedArgs);
11 },
12};
13
14const double = new Proxy(
15 (a, b) => a + b,
16 handler
17);
18
19console.log(double(2, 3)); // Calling..., returns 10 (4 + 6)construct Trap (Classes)#
1const handler = {
2 construct(target, argumentsList, newTarget) {
3 console.log(`Creating new instance with: ${argumentsList}`);
4
5 // Modify constructor arguments
6 const instance = Reflect.construct(target, argumentsList, newTarget);
7
8 // Add extra properties
9 instance.createdAt = new Date();
10
11 return instance;
12 },
13};
14
15class User {
16 constructor(name) {
17 this.name = name;
18 }
19}
20
21const ProxiedUser = new Proxy(User, handler);
22const user = new ProxiedUser('John');
23console.log(user.createdAt); // Date objectgetOwnPropertyDescriptor Trap#
1const handler = {
2 getOwnPropertyDescriptor(target, property) {
3 const descriptor = Reflect.getOwnPropertyDescriptor(target, property);
4
5 // Hide private properties
6 if (property.startsWith('_')) {
7 return undefined;
8 }
9
10 // Modify descriptor
11 if (descriptor) {
12 descriptor.enumerable = true;
13 }
14
15 return descriptor;
16 },
17};
18
19const obj = new Proxy({ name: 'John', _secret: 'hidden' }, handler);
20console.log(Object.getOwnPropertyDescriptor(obj, 'name'));
21console.log(Object.getOwnPropertyDescriptor(obj, '_secret')); // undefineddefineProperty Trap#
1const handler = {
2 defineProperty(target, property, descriptor) {
3 // Prevent non-writable properties
4 if (descriptor.writable === false) {
5 console.log('Non-writable properties not allowed');
6 return false;
7 }
8
9 // Validate property names
10 if (property.startsWith('_')) {
11 console.log('Properties cannot start with _');
12 return false;
13 }
14
15 return Reflect.defineProperty(target, property, descriptor);
16 },
17};
18
19const obj = new Proxy({}, handler);
20
21Object.defineProperty(obj, 'name', {
22 value: 'John',
23 writable: true,
24}); // OK
25
26Object.defineProperty(obj, '_private', {
27 value: 'hidden',
28}); // FailsgetPrototypeOf / setPrototypeOf Traps#
1const handler = {
2 getPrototypeOf(target) {
3 console.log('Getting prototype');
4 return Reflect.getPrototypeOf(target);
5 },
6
7 setPrototypeOf(target, prototype) {
8 // Prevent prototype changes
9 console.log('Prototype is immutable');
10 return false;
11 },
12};
13
14const obj = new Proxy({}, handler);
15Object.getPrototypeOf(obj); // "Getting prototype"
16Object.setPrototypeOf(obj, null); // Fails - "Prototype is immutable"isExtensible / preventExtensions Traps#
1const handler = {
2 isExtensible(target) {
3 console.log('Checking extensibility');
4 return Reflect.isExtensible(target);
5 },
6
7 preventExtensions(target) {
8 console.log('Preventing extensions');
9 return Reflect.preventExtensions(target);
10 },
11};
12
13const obj = new Proxy({}, handler);
14console.log(Object.isExtensible(obj)); // true
15Object.preventExtensions(obj);
16console.log(Object.isExtensible(obj)); // falseComplete Observable Object#
1function createObservable(target, onChange) {
2 const handler = {
3 get(target, property, receiver) {
4 const value = Reflect.get(target, property, receiver);
5
6 // Return proxy for nested objects
7 if (typeof value === 'object' && value !== null) {
8 return createObservable(value, (path, newVal, oldVal) => {
9 onChange(`${property}.${path}`, newVal, oldVal);
10 });
11 }
12
13 return value;
14 },
15
16 set(target, property, value, receiver) {
17 const oldValue = target[property];
18 const result = Reflect.set(target, property, value, receiver);
19
20 if (result && oldValue !== value) {
21 onChange(property, value, oldValue);
22 }
23
24 return result;
25 },
26
27 deleteProperty(target, property) {
28 const oldValue = target[property];
29 const result = Reflect.deleteProperty(target, property);
30
31 if (result) {
32 onChange(property, undefined, oldValue);
33 }
34
35 return result;
36 },
37 };
38
39 return new Proxy(target, handler);
40}
41
42// Usage
43const state = createObservable(
44 { user: { name: 'John' }, count: 0 },
45 (path, newVal, oldVal) => {
46 console.log(`${path}: ${oldVal} -> ${newVal}`);
47 }
48);
49
50state.count = 1; // count: 0 -> 1
51state.user.name = 'Jane'; // user.name: John -> JaneBest Practices#
Trap Usage:
✓ Use Reflect methods for defaults
✓ Return correct values
✓ Maintain invariants
✓ Handle edge cases
Performance:
✓ Keep handlers simple
✓ Cache proxies when possible
✓ Avoid deep nesting
✓ Consider alternatives for hot paths
Patterns:
✓ Validation and access control
✓ Logging and debugging
✓ Observable objects
✓ Default values
Avoid:
✗ Breaking object invariants
✗ Infinite recursion
✗ Hiding errors silently
✗ Over-engineering simple cases
Conclusion#
Proxy handlers provide fine-grained control over object operations through traps like get, set, has, and deleteProperty. Always use Reflect methods to maintain default behavior and return appropriate values. Proxies are powerful for validation, observation, access control, and virtualization patterns, but should be used judiciously as they add overhead and complexity.