How to Master the Proxy API in 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.
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:
const man = new Proxy({}, {
get: (object, property) => property in object ? object[property] : {};
});
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.
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.
const emptyObjectFallback = target => new Proxy(target, {
get: (object, property) => property in object ? object[property] : {};
});
let man = {
name: "Arthur"
};
man = emptyObjectFallback(man);
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.
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.
const noUndefinedProps = target => new Proxy(target, {
get(object, property) {
if (property in object) {
return object[property];
} else {
console.error(`${object.name} has no ${property}`);
}
}
});
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.
Smart arrays
We can also improve array lookups by implementing a custom logic for the get trap.
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(['🍩', '🍰', '🍪']);
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.
We can further enhance this to also support intervals.
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) ["🍰", "🍪"]
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.
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);
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.
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);
}
});
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.
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.
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
});
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.
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.
const parseStrings = target => new Proxy(target, {
set(object, property, value) {
if (/^\d+$/.test(value)) {
value = +value;
}
return Reflect.set(...arguments);
}
});
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.
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!
Rocket Launch Your Career
Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies: