And finally, I've found a real use-case of a Proxy object in JavaScript. Many developers love to use enums
in their code but as a JavaScript developer, you don't have the opportunity to use that cool feature.
As there is no such feature in JavaScript, let's create a custom implementation of creating an enum. No more talking, deep dive into the code.
const createEnum = (structure) => {
if (structure === null || typeof structure !== 'object' || Array.isArray(structure)) {
throw Error(`'${structure}' is not a valid enum structure.`);
}
for (const key in structure) {
if (!['number', 'string', 'boolean'].includes(typeof structure[key])) {
throw Error(
`You are only allowed to use 'number', 'string' or 'boolean' types, but you are using '${JSON.stringify(
structure[key]
)}'`
);
}
}
return new Proxy(structure, {
set(target, prop) {
if (Reflect.has(target, prop)) {
throw Error(`Cannot assign to '${prop}' because it is a read-only property.`);
} else {
throw Error(`Property '${prop}' does not exist on the enum structure.`);
}
},
get(target, prop) {
return Reflect.get(target, prop);
},
});
};
Create the function
Here, we are creating a createEnum
function, that accepts a parameter structure
. This structure
is a plain object like [key: string]: number | string | boolean
. So, what does it mean?
This means the structure
object only supports a string type key and a string or number or boolean type value. For example-
const structure = {
PENDING: 'pending',
ACCEPTED: 'accepted',
REJECTED: 'rejected'
}
As we are trying to create a utility function and want other developers to use that function, so, we have to do some sanitization.
First check if the structure is an object or not.
if (structure === null || typeof structure !== 'object' || Array.isArray(structure)) {
throw Error(`'${structure}' is not a valid enum structure.`);
}
In the code above, we are checking if the structure
is an object or not. We can easily check whether a variable is an object or not by checking its type (typeof structure === 'object'
). But the problem is typeof null
and typeof []
also return 'object'. So, we have to keep in mind those cases as well.
If the structure is an object, then check the object is a valid one.
for (const key in structure) {
if (!['number', 'string', 'boolean'].includes(typeof structure[key])) {
throw Error(
`You are only allowed to use 'number', 'string' or 'boolean' types, but you are using '${JSON.stringify(
structure[key]
)}'`
);
}
}
Previously we are checking whether the structure
param is an object or not. If this is an object, then we have to check the validity of that object's values. For an enum, we can only assign string | number | boolean
type values.
In the code above we are checking all the properties of the structure
object, and if there is any value that exists which is not a number
or string
or boolean
then throw an error.
Finished the sanitization. Now it's time to create the Proxy
.
We've completed our sanitization for the structure
param. Now create the enum.
Our goals are -
- Creating an object which is not extendable and changeable a.k.a not modifiable.
- If someone tries to modify it, then throw an error.
We can easily prevent an object from being modified by using the Object.freeze()
function. But the problem is that Object.freeze()
won't throw an error if you try to change a property or add a new property. Then how could we do that?
Here, the Proxy
comes as a rescuer.
In short, a Proxy
accepts a target object and can listen to any change in the target object and return a new object. For more about proxy you can read this.
One of our goals is to prevent the modification of the structure
object's properties and throw errors if any modification happens. By using the set
handler function we can do it easily.
return new Proxy(structure, {
set(target, prop) {
if (Reflect.has(target, prop)) {
throw Error(`Cannot assign to '${prop}' because it is a read-only property.`);
} else {
throw Error(`Property '${prop}' does not exist on the enum structure.`);
}
},
get(target, prop) {
return Reflect.get(target, prop);
},
});
Inside the set
handler function we are simply throwing some errors and this simple trick prevents us to modify any property to that object. Here, we are checking if the changing property already exists in the object or not. If it exists that means we are trying to change an existing property, otherwise we are trying to add some new property. In these cases, we are throwing two separate errors.
And also, a question may arise in your mind, what the heck is this Reflect
thing doing here? According to MDN
Reflect is a built-in object that provides methods for >interceptable JavaScript operations. The methods are the same as those of proxy handlers. Reflect is not a function object, so it's not constructible.
The get
function of the proxy handler is just returning the respective value of a property.
Doing a lot of things. Now it's time to test the function.
Yes, this is the exciting moment of testing our implementation. Let's do it.
const Status = createEnum({
PENDING: 'pending',
ACCEPTED: 'accepted',
REJECTED: 'rejected'
});
Bingo! we have created our first enum π. Now we can use that enum like Status.PENDING
, Status.ACCEPTED
, ... etc. But wait, how does it being different from a regular object?
I think you've already guessed the difference. If we try to do something like this-
// β Invalid assignment
Status.PENDING = 'loading'; //Error: Cannot assign to 'PENDING' because it is a read-only property.
Status.NEW_VALUE = 'new-value'; //Error: Property 'NEW_VALUE' does not exist on the enum structure.
This change will cause you an error that does not happen for a normal object.
See a real use-case for this enum.
const user = {
id: '3423',
name: 'John Doe',
email: 'john@example.com',
status: Status.PENDING,
};
// β
The condition is true
if (user.status === Status.PENDING) {
console.log('Pending user');
}
Here, we used the enum to an almost real scenario π. We declared a user
object and for the status
property, we assigned a value from the Status
enum. Also, we are checking the user.status
with the Status
enum.
We did it... π π π
Conclusion
From this article we are going to know about Enums
, Proxy
, Reflect
and many more π. An enum is very useful for writing clean and readable code. Here, we've created an enum using JavaScript Proxy
object and prevent the enum from being modified externally. For doing that we've used Proxy's set
and get
handler functions. And finally, we can achieve what we are expecting.
Top comments (3)
If you enable strict mode, your object freeze assignement will throw a TypeError exception :
Yeap, exactly. But for the non-strict mode it will be silent but still a frozen object is not updated or extended.
But still you can throw your own generated errors messages, and also effective where the strict mode is not enabled.