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:
1interface MyApi {2 /**3 * Adds 2 numbers4 * @returns the result of addition5 */6 add: (a: number, b: number) => number;7
8 /**9 * Difference between 2 numbers10 * @returns the result of subtraction11 */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:
1const 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:
1const 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 object8 return target[key];9 },10});11
12logAccess.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
1type WithoutIllegal<T> = Omit<T, "illegal">;2
3const 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
13try {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 throw18 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:
1const logs: any[][] = [];2const 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
18withLogs.add(1, 2);19withLogs.subtract(1, 2);20withLogs.illegal(1, 2);21
22console.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:
1const fake = new Proxy<Record<string, Record<string, string>>>(2 {},3 {4 get(target, property) {5 return {6 [property]: property,7 };8 },9 }10);11
12console.log(fake);13// {}14
15console.log(fake.name);16// { name: 'name' }17
18console.log(fake.age);19// { age: 'age' }20
21console.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:
1const 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:
1const tracker: any[][] = [];2const 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 call17 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 accessed31 return createTracker(nxt as object, path);32 },33 });34};35
36const tracked = createTracker(deepObject);37
38const result = tracked.a.b.getC().c(1, 2);39
40console.log({ result });41// { result: { answer: 3 } }42
43console.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// ]