Create an Enum representation in JavaScript using Proxy object.
Tags: javascript, typescript, webdev, showdev
Published: 2022-08-09 19:03:00

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.

```js
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- 
```js
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.
```js
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.
```js
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)._

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.

```js
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect)

>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.

```js
const Status = createEnum({
    PENDING: 'pending',
    ACCEPTED: 'accepted',
    REJECTED: 'rejected'
});
```

Bingo! we have created our first enum :tada:. 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- 

```js
// ❌ 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.

```js
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 :stuck_out_tongue:. 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... :tada: :tada: :tada:

## Conclusion
From this article we are going to know about `Enums`, `Proxy`, `Reflect` and many more :wink:. 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.