How to Master the Proxy API in JavaScript

How to Master the Proxy API in JavaScript

Looking at practical use-cases of metaprogramming
Ferenc AlmasiLast updated 2021 November 11 • Read time 11 min read
Get your weekly dose of webtips
  • twitter
  • facebook
JavaScript

JavaScript is a pretty flexible language. It is loosely typed and dynamic in nature. You’ve probably already know that its first version — back then known as Mocha — was created in only 10 days. Being this flexible meant that developers could easily extend it to cater for their custom use-cases.

With the Proxy API, this flexibility is taken to a new level. You can hook into the meta-level to change the behavior of JavaScript itself.


What is Metaprogramming?

Metaprogramming can mean several things. But in the context of the Proxy API, it means that the program can modify itself during execution.

With the help of the Proxy API, you have the ability to redefine the semantics and behavior of fundamental operations like property lookup or assignments. That is all that the Proxy API is. So how does it work?


It’s a Trap

Proxies are using so-called “traps” to give custom behavior to operations. They are methods that intercepts the operation, just like a proxy server would intercept a network request. Let’s take a look at a couple of examples to see how they work.

Analogy of the Proxy API that sits in between a property lookup
The Proxy API sits in between a property lookup and produces an error if a property is not defined
Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScriptinfo Remove ads

Getting Values

First, let’s take a look at how we can intercept property lookup and provide custom behavior when we try to read a property.

Defaulting to custom values

Imagine that you want to build your logic around returning an empty object, whenever one of your properties is not defined. You don’t want to use undefined, you want to have a fallback value. Which in this case is an empty {}. We can do this in the following way:

Copied to clipboard!
const man = new Proxy({}, {
    get: (object, property) => property in object ? object[property] : {};
});
proxy.js

We assign man to a new Proxy. The proxy accepts two arguments:

  • Target: The first argument is the target object that we want to wrap into a Proxy. It can be even an array, a function, or another Proxy. Here we use an empty object.
  • Handler: The second argument is an object that has predefined methods. We can use these methods to define custom behavior for the operations. They are called traps. The get trap is used for getting property values.

In this example, we simply check if the property exists on the object. If it’s not, we return with an empty object.

proxy property lookup

If you request this in the console, you can see that man is a Proxy. Whenever I try to access a non-existent property, it will return an empty object.

To make things more readable, we can outsource this call into a function. This way, we can reuse it later whenever we need it.

Copied to clipboard! Playground
const emptyObjectFallback = target => new Proxy(target, {
    get: (object, property) => property in object ? object[property] : {};
});

let man = {
    name: "Arthur"
};

man = emptyObjectFallback(man);
proxy.js

If we wrap an object into this function, it will return a Proxy with the new functionality. This also hides the underlying logic and makes things more readable. The intention is clearly conveyed.

outsourcing the Proxy into a function

Alerting

Let’s see a more practical example. Now you want to know whenever your properties are not defined. You can put some alerting in place with it.

Copied to clipboard! Playground
const noUndefinedProps = target => new Proxy(target, {
    get(object, property) {
        if (property in object) {
            return object[property];
        } else {
            console.error(`${object.name} has no ${property}`);
        } 
    }
});
proxy.js

Calling this with the same object as before, you will get an error logged out to the console whenever a property is referenced that is not defined. This makes it easier to spot potential issues in your application.

If we don’t have a property, the proxy will print out an error

Smart arrays

We can also improve array lookups by implementing a custom logic for the get trap.

Copied to clipboard! Playground
const smartArray = target => new Proxy(target, {
    get(object, property) {
        if (+property >= 0) {
            return Reflect.get(...arguments);
        } else {
            return object[object.length + +property];
        }
    }
});

const sweets = smartArray(['🍩', '🍰', '🍪']);
proxy.js

This trap will get the original value — using the Reflect API — if the index is a positive number. If we pass in a negative number, however, we can retrieve values from the end of the array.

Improving array usability with the get trap, using the Proxy API

We can further enhance this to also support intervals.

Copied to clipboard! Playground
const smartArray = target => new Proxy(target, {
    get(object, property) {
        if (+property >= 0) {
            return Reflect.get(...arguments);
        } else {
            if (property.includes('-')) {
                const from = +property.split('-')[0];
                const to   = +property.split('-')[1] + 1;
                
                return object.slice(from, to);
            } else {
                return object[object.length + +property];
            }
        }
    }
});

const sweets = smartArray(['🍩', '🍰', '🍪']);

sweets['0-1'] // Returns (2) ["🍩", "🍰"]
sweets['1-2'] // Returns (2) ["🍰", "🍪"]
proxy.js

Smart objects

The same can we done with objects. Say you want to reach a deeply nested property, without having to write out the full path. You only know the name of the object, and the property you are looking for. We can do this with the help of a recursive function.

Copied to clipboard! Playground
let value = null;

const searchFor = (property, object) => {
    for (const key of Object.keys(object)) {
        if (typeof object[key] === 'object') {
            searchFor(property, object[key]);
        } else if (typeof object[property] !== 'undefined') {
            value = object[property];
            break;
        }
    }

    return value;
};

const smartObject = target => new Proxy(target, {
    get(object, property) {
        if (property in object) {
            return object[property]
        } else {
            return searchFor(property, object);
        }
    }
});

const data = smartObject({
    user: {
        id: 1,
        settings: {
            theme: 'light'
        }
    }
});

// The two will be equivalent, both will return "light"
console.log(data.user.settings.theme);
console.log(data.theme);
proxy.js

Now you can reach properties, just like they would exist on the top level. The question is, should you?


Setting Values

Like the get trap can be used to intercepting property lookup, the set trap can be used for assignments. Everything remains the same, except we need to use set instead of get. Let’s have a look.

Validating properties

The most common case is to validate properties. Let’s create a validator that validates a user object.

Copied to clipboard! Playground
const validatedUser = target => new Proxy(target, {
    set(object, property, value) {
        switch(property) {
            case 'email':
                const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
                if (!regex.test(value)) {
                    console.error('The user must have a valid email');
                    return false;
                }
                break;
            case 'age':
                if (value < 18 || value > 65) {
                    console.error('A user\'s age must be between 18 and 65');
                    return false;
                }
                break;
        }

        return Reflect.set(...arguments);
    }
});
proxy.js

Unlike the get trap, set has three parameters. One for the object, one for the property and one for its value. As you can see, we always have to return something.

If some of the validation fails, we return false. This will prevent the property to be set. At the end we can use the Reflect API, to set the value if the validation was successful.

Validating an object with the set trap

As you can see, the value won’t be set if the email is invalid. The same is true for age.

Making properties read-only

We can also use the trap to create read-only properties. This way, you can ensure that no one can change them.

Copied to clipboard! Playground
const preventWrite = () => {
    console.error('The object you try to modify is read-only');
};

const readOnly = target => new Proxy(target, {
    set:            preventWrite,
    deleteProperty: preventWrite,
    defineProperty: preventWrite,
    setPropertyOf:  preventWrite
});
proxy.js

Here we used a function that’s only purpose is to write an error to the console. The proxy not only prevents assignments, but delete or any kind of extension.

The proxy makes the object read-only

You can also put some custom logic in place to make only a selected number of properties read-only.

Converting strings to numbers

Another use-case would be to automatically convert strings to numbers. This can be done with some regex magic.

Copied to clipboard! Playground
const parseStrings = target => new Proxy(target, {
    set(object, property, value) {
        if (/^\d+$/.test(value)) {
            value = +value;
        }
        
        return Reflect.set(...arguments);
    }
});
proxy.js

All we need is an if statement to reassign the value if it only contains numbers. Now whenever you assign a value that is a string, but only contains numbers, the proxy will convert it for us.

Parsing strings to numbers with Proxy
If the string only contains numbers, it will be converted to an integer

Conclusion

Now you should have a pretty strong foundation about the Proxy API in JavaScript. Once you get the hang of it, this can be a pretty powerful tool to enhance your everyday operations.

Do you know other use-cases that the Proxy API can be used for and is not mentioned in this tutorial? Let us know in the comments! Thank you for taking the time to read this article, happy coding!

  • twitter
  • facebook
JavaScript
Did you find this page helpful?
📚 More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Access 100+ interactive lessons
  • check Unlimited access to hundreds of tutorials
  • check Prepare for technical interviews
Become a Pro

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.