panzerdp / dmitripavlutin.com-comments

7 stars 0 forks source link

javascript-enum/ #165

Open utterances-bot opened 1 year ago

utterances-bot commented 1 year ago

4 Ways to Create an Enum in JavaScript

An enum type represents a set of constants. The plain object, frozen object, proxied object or class-based are the 4 ways to create enums in JavaScript.

https://dmitripavlutin.com/javascript-enum/

opensas commented 1 year ago

nice article

perhaps you could also cover how to do it in Typescript

this is my preferred way right now

let sizes = ['small', 'medium', 'large'] as const

type Size = typeof sizes[number] // 'small' | 'medium' | 'large

// Type '"mediaum"' is not assignable to type '"small" | "medium" | "large"'. Did you mean '"medium"'?(2820)
const size: Size = 'mediaum'
panzerdp commented 1 year ago

nice article

Thanks @opensas!

perhaps you could also cover how to do it in Typescript. this is my preferred way right now

Why not use the regular TypeScript enum type?

va-deem commented 1 year ago

Why not use the regular TypeScript enum type?

Event TypeScript documentation says that TS enums may be not the best choice.

In modern TypeScript, you may not need an enum when an object with as const could suffice

As I see many developers now are not using TS enums (so am I)

va-deem commented 1 year ago

Nice article, by the word. I didn't know about Proxy enums. Thanks!

octavian-nita commented 1 year ago

Nice line-up of techniques!

One other advantage I see with class-based enums is that you can attach value-specific functionality (i.e., define methods that all enum values have but they behave differently for each enum, polymorphism-like).

Moreover, I guess you can control/disallow new value creation in the constructor, right?

panzerdp commented 1 year ago

Event TypeScript documentation says that TS enums may be not the best choice.

@va-deem Good point. TypeScript recommends objects as const as an alternative to enums:

const ODirection = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;

Nice article, by the word. I didn't know about Proxy enums. Thanks!

You're welcome!

panzerdp commented 1 year ago

Nice line-up of techniques!

@octavian-nita Thanks!

One other advantage I see with class-based enums is that you can attach value-specific functionality (i.e., define methods that all enum values have but they behave differently for each enum, polymorphism-like).

You can customize further the class-based enum.

Moreover, I guess you can control/disallow new value creation in the constructor, right?

Good idea! It's possible to control the number of created enum instances using a static field (e.g. instancesCount).

opensas commented 1 year ago

@panzerdb I @va-deem comments, it's not so popular nowadays, and I don't like all the code that typescript generates behind it, I still like to think about typescript as a compile time only thing.

I'm sure the generated code should work ok, but just knowing that thing is there unnerves me (jaja!)

let sizes = ['small', 'medium', 'large'] as const

type Size = typeof sizes[number] // 'small' | 'medium' | 'large

// Type '"mediaum"' is not assignable to type '"small" | "medium" | "large"'. Did you mean '"medium"'?(2820)
const size: Size = 'mediaum'

// using enum
enum SizeEnum { small, medium, large }

const size2: SizeEnum = SizeEnum.small

// ---------------------------
// generated code:

"use strict";

let sizes = ['small', 'medium', 'large'];
// Type '"mediaum"' is not assignable to type '"small" | "medium" | "large"'. Did you mean '"medium"'?(2820)
const size = 'mediaum';

// code generated using enum
var SizeEnum;
(function (SizeEnum) {
    SizeEnum[SizeEnum["small"] = 0] = "small";
    SizeEnum[SizeEnum["medium"] = 1] = "medium";
    SizeEnum[SizeEnum["large"] = 2] = "large";
})(SizeEnum || (SizeEnum = {}));
const size2 = SizeEnum.small;
pspaulding commented 1 year ago

I've used all of these approaches in the past (and then some), but my current favorite is:

enums/Size.js

export const small = 'small';
export const medium = 'medium';
export const large = 'large';

Use the types:

import * as Size from 'enums/Size';

const foo = Size.medium;
  1. Easy to understand (no "magic").
  2. Immutability (can't reassign types), and compile/build time detection of typos.
  3. Most IDEs can understand the syntax and provide code completion and detection of typos (red squiggly in VS Code).
panzerdp commented 1 year ago

I've used all of these approaches in the past (and then some), but my current favorite is:

@pspaulding An original way to create enums! Thanks for sharing.

opensas commented 1 year ago

Now that I think about it, what I find myself doing most often nowadays is just using the enum from the type of the structure where is id being effectively used, if it is being used by more than one structure I might extract it to it's own type, something like this:

export type Card = {
  title: string;
  size: 'small' | 'medium' | 'large
  ...
}

And then I might use it like

  let size: Card['size'] = 'small'

for example

panzerdp commented 1 year ago

Now that I think about it, what I find myself doing most often nowadays is just using the enum from the type of the structure where is id being effectively used, if it is being used by more than one structure I might extract it to it's own type, something like this

@opensas In TypeScript, I'd probably go with a union of string literals too.

moloko commented 1 year ago

We used to use the class-based technique all the time back in the days of ActionScript! I've actually really missed being able to use that technique...

One other pro of using a class-based Enum is that you can also use static methods to convert from a primitive data type into the Enum e.g. const size = Sizes.fromString('small') or const myColor = Colors.fromHex('#FF0000')

panzerdp commented 1 year ago

One other pro of using a class-based Enum is that you can also use static methods to convert from a primitive data type into the Enum e.g. const size = Sizes.fromString('small') or const myColor = Colors.fromHex('#FF0000')

@moloko That's very true. Thanks for sharing.

KooiInc commented 1 year ago

Here's a small module to create/use flagged enums.

joshamaju commented 1 year ago

@pspaulding

// enums/Size.js

export const small = 'small';
export const medium = 'medium';
export const large = 'large';

// Use the types:

import * as Size from 'enums/Size';

I used to do that until I realised that it's not tree shakeable. The object approach is actually the best, cause it's tree shakeable. Seems counter intuitive, I didn't believe it until I experimented with it.

e.g

let user_size = Size.small

will get transpiled to

let user_size = "small"

Bundlers are able to understand how you use a const Size {small: "small", ...} as const than your stated approach. So they can get rid of the other enum members if you don't use them and inline the ones you do use.

wiolowan-code commented 12 months ago
const Size = Enum( 'BIG', 'MID', 'SMALL' )
//console.log(Size): Proxy(Object) {BIG: Symbol(BIG), MID: Symbol(MID), SMALL: Symbol(SMALL)}

Size.BIG !== Size.MID // true

function Enum( ...list ) {
    const enumObj = Object.create(null)
    list.forEach( el => enumObj[el] = Symbol(el) )
    return new Proxy(enumObj, {
        get(target, name) {
            if (!Object.hasOwn(enumObj, name)) {
                throw new Error(`"${name}" value does not exist in the enum`)
            }
            return enumObj[name]
        },
        set(target, name, value) {
            throw new Error('Cannot add a new value to the enum')
        },
        deleteProperty(target, name) {
            throw new Error('Cannot delete a property for the enum')
        }
    })
}
wiolowan-code commented 12 months ago

Modifying

list.forEach( el => enumObj[el] = Symbol(el) )

to

list.flat().forEach( el => enumObj[el] = Symbol(el) )

in the above enumeration factory we enable it to accept both arguments list and arrays:

const Size = Enum( 'BIG', 'MID', 'SMALL' )
const Size2 = Enum( [ 'BIG', 'MID', 'SMALL' ] )

(yielding the same result: // Proxy(Object) {BIG: Symbol(BIG), MID: Symbol(MID), SMALL: Symbol(SMALL)}

wheatbrad commented 5 months ago

Nice article.

I had been using Object.create() with a null prototype and property descriptors to make sure the values are read only. By default the properties are not writable or configurable.

const Size = Object.create(null, {
  Small:  { value: 0 },
  Medium: { value: 1},
  Large:  { value: 2}
};