Javascript Proxy Object

11 December 2024

Updated: 11 December 2024

Proxies allow us to wrap existing object and modify the behavior when interacting with them in some interesting ways. This post will take a look at a few things we can use them for

Something to work with

For the sake of this example we’ll be using Typescript. Let’s create a reference object type that we will interact with - we’re going to call this MyApi and it’s defined simply as an object with a few methods on it:

1
interface MyApi {
2
/**
3
* Adds 2 numbers
4
* @returns the result of addition
5
*/
6
add: (a: number, b: number) => number;
7
8
/**
9
* Difference between 2 numbers
10
* @returns the result of subtraction
11
*/
12
subtract: (a: number, b: number) => number;
13
14
/**
15
* (secretly does some illegal stuff)
16
*/
17
illegal: (a: number, b: number) => number;
18
}

Initial implementation

We can implement a simple object that satisifies this APi as below:

1
const baseApi: MyApi = {
2
add(a, b) {
3
return a + b;
4
},
5
6
subtract(a, b) {
7
return a - b;
8
},
9
10
illegal(a, b) {
11
return a / b;
12
},
13
};

Log accesses

For this example we’ll consider the illegal method as special. We’ll want to track each time the illegal property is accessed. Using a Proxy we can wrap the baseApi and provide a get method that will handle property access and allow us to see what property of our object is being accessed

When illegal is accessed, we log out a message:

1
const logAccess = new Proxy<MyApi>(baseApi, {
2
get(target, key: keyof MyApi) {
3
if (key === "illegal") {
4
console.log("Tried to access illegal method");
5
}
6
7
// return the property from the original object
8
return target[key];
9
},
10
});
11
12
logAccess.illegal(1, 2);
13
// logs out the message before calling the function

Prevent illegal access

We can also do stuff like create a type of WithoutIllegal version of MyApi in which we remove the property from the type definition. Additionally, we can also make this throw an error if someone attempts to access it at runtime as can be seen below. This is very similar to the previous example but now we have a direct impact on the consumer

1
type WithoutIllegal<T> = Omit<T, "illegal">;
2
3
const police = new Proxy<WithoutIllegal<MyApi>>(baseApi, {
4
get(target, key: keyof MyApi) {
5
if (key === "illegal") {
6
throw new Error("accessing illegal properties is a crime");
7
}
8
9
return target[key];
10
},
11
});
12
13
try {
14
// @ts-expect-error this is now an error since we say "illegal is not defined"
15
police.illegal;
16
17
// @ts-expect-error if we try to access it, it will throw
18
police.illegal(1, 2);
19
} catch (err) {
20
console.error("Got illegal access", err);
21
}

Interact with other objects

During the proxying process, we can also do things like interact with objects that aren’t defined within our base object itself:

1
const logs: any[][] = [];
2
const withLogs = new Proxy<MyApi>(baseApi, {
3
get<K extends keyof MyApi>(target: MyApi, key: K) {
4
const method = target[key];
5
6
return (a: number, b: number) => {
7
logs.push(["accessing", key, a, b]);
8
9
const result = method(a, b);
10
11
logs.push(["result", result]);
12
13
return result;
14
};
15
},
16
});
17
18
withLogs.add(1, 2);
19
withLogs.subtract(1, 2);
20
withLogs.illegal(1, 2);
21
22
console.log(logs);
23
// [
24
// [ 'accessing', 'add', 1, 2 ],
25
// [ 'result', 3 ],
26
// [ 'accessing', 'subtract', 1, 2 ],
27
// [ 'result', -1 ],
28
// [ 'accessing', 'illegal', 1, 2 ],
29
// [ 'result', 0.5 ]
30
// ]

Creating fake objects

We can also create an object in which any properties can exist and have a specific value, for example an object that has this structure for any key given:

1
{
2
myKey: "myKey"
3
}

This can be done like so:

1
const fake = new Proxy<Record<string, Record<string, string>>>(
2
{},
3
{
4
get(target, property) {
5
return {
6
[property]: property,
7
};
8
},
9
}
10
);
11
12
console.log(fake);
13
// {}
14
15
console.log(fake.name);
16
// { name: 'name' }
17
18
console.log(fake.age);
19
// { age: 'age' }
20
21
console.log(fake.somethingelse);
22
// { somethingelse: 'somethingelse' }

It’s also interesting to note that the fake object has no direct properties, and we cxan see that when we log it

Recursive proxies

Proxies can also return other proxies. This allows us to proxy objects recursively. For example, given the following object:

1
const deepObject = {
2
a: {
3
b: {
4
getC: () => ({
5
c: (x: number, y: number) => ({
6
answer: x + y,
7
}),
8
}),
9
},
10
},
11
};

We can create a proxy that tracks different actions, such as property acccess. Recursive proxies can be created by returning a new proxy at the levels that we care about. In the below example, we create a proxy for every property that we access as well as for the result of every function call:

1
const tracker: any[][] = [];
2
const createTracker = <T extends object>(obj: T, prefix: string = ""): T => {
3
return new Proxy<T>(obj, {
4
apply(target, thisArg, argArray) {
5
tracker.push(["call", prefix, argArray]);
6
7
const bound = (target as (...args: any[]) => any).bind(thisArg);
8
const result = bound(...argArray);
9
10
tracker.push(["return", prefix, result]);
11
12
if (typeof result === "undefined") {
13
return result;
14
}
15
16
// create a new proxy around the object that's returned from a function call
17
return createTracker(result, prefix);
18
},
19
20
get(_, prop: keyof T & string) {
21
const path = `${prefix}/${prop}`;
22
23
tracker.push(["accessed", path]);
24
const nxt = obj[prop];
25
26
if (typeof nxt === "undefined") {
27
return nxt;
28
}
29
30
// create a new proxy around the object that's being accessed
31
return createTracker(nxt as object, path);
32
},
33
});
34
};
35
36
const tracked = createTracker(deepObject);
37
38
const result = tracked.a.b.getC().c(1, 2);
39
40
console.log({ result });
41
// { result: { answer: 3 } }
42
43
console.log(tracker);
44
// [
45
// [ 'accessed', '/a' ],
46
// [ 'accessed', '/a/b' ],
47
// [ 'accessed', '/a/b/getC' ],
48
// [ 'call', '/a/b/getC', [] ],
49
// [ 'return', '/a/b/getC', { c: [Function: c] } ],
50
// [ 'accessed', '/a/b/getC/c' ],
51
// [ 'call', '/a/b/getC/c', [ 1, 2 ] ],
52
// [ 'return', '/a/b/getC/c', { answer: 3 } ]
53
// ]

References

MDN Proxy Docs