DEV Community

Sajeeb Ahamed
Sajeeb Ahamed

Posted on

Create an Enum representation in JavaScript using Proxy object.

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);
        },
    });
};
Enter fullscreen mode Exit fullscreen mode

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'
}
Enter fullscreen mode Exit fullscreen mode

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.`);
}
Enter fullscreen mode Exit fullscreen mode

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]
      )}'`
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  },
});
Enter fullscreen mode Exit fullscreen mode

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'
});
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
ecyrbe profile image
ecyrbe

If you enable strict mode, your object freeze assignement will throw a TypeError exception :

"use strict";
const Status = Object.freeze({
    PENDING: 'pending',
    ACCEPTED: 'accepted',
    REJECTED: 'rejected'
});
Status.ACCEPTED = 'test'; // TypeError
Enter fullscreen mode Exit fullscreen mode
Collapse
 
smlka profile image
Andrey Smolko

Yeap, exactly. But for the non-strict mode it will be silent but still a frozen object is not updated or extended.

Collapse
 
ahamed_ profile image
Sajeeb Ahamed

But still you can throw your own generated errors messages, and also effective where the strict mode is not enabled.