DEV Community

Discussion on: Typescript Enums are bad!!1!!!1!!one - Are they really?

Collapse
peerreynders profile image
peerreynders

Objects vs Enums

"The biggest argument in favour of this format over TypeScript’s enum is that it keeps your codebase aligned with the state of JavaScript"

Collapse
dvddpl profile image
Davide de Paolis Author

Personally I find that section very confusing. We are not comparing an Obiect with an enum, rather an "Object with as const".

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

type Direction = typeof ODirection[keyof typeof ODirection]
Enter fullscreen mode Exit fullscreen mode

looks simply weird and ugly to me.
Not a great gain

Collapse
peerreynders profile image
peerreynders

what I am looking at while reviewing and debugging is Typescript not Javascript.

Perhaps this is the core issue.

TypeScript is nothing but a JavaScript dialect.

If JavaScript cannot easily use code output from TypeScript then TypeScript has failed.

I view TypeScript as a JavaScript with some additional information so that the tooling can perform static type checking - or rather static type linting.

There is the notion of type declaration space and variable declaration space:

  • type space: where TypeScript compile time types live
  • value space: where JavaScript runtime values live

The unfortunate thing is that TypeScript source code conflates both type and value space.

const userStatus = {
  REGISTERED: 'registered',
  INACTIVE: 'inactive',
  NOT_FOUND: 'notFound',
  BANNED: 'banned',
};
Enter fullscreen mode Exit fullscreen mode

This is how one might gather together related constants in JavaScript (value space) - in effect emulating an enum.

const userStatus = {
  REGISTERED: 'registered',
  INACTIVE: 'inactive',
  NOT_FOUND: 'notFound',
  BANNED: 'banned',
} as const;
Enter fullscreen mode Exit fullscreen mode

as const a const assertion is the first bit of "type space" information. It conveys that modifying userStatus would be an error; as a consequence of being read-only we can now base new literal types on this read-only value.

typeof userStatus creates a new type based on the whole userStatus value (typeof type operator).

type TypeUserStatus = typeof userStatus;

/* 
type TypeUserStatus = {
  readonly REGISTERED: "registered";
  readonly INACTIVE: "inactive";
  readonly NOT_FOUND: "notFound";
  readonly BANNED: "banned";
}

Note without `as const`:

type TypeUserStatus = {
    REGISTERED: string;
    INACTIVE: string;
    NOT_FOUND: string;
    BANNED: string;
}
*/
Enter fullscreen mode Exit fullscreen mode

keyof typof userStatus extracts the object property keys as a union (keyof type operator).

type UserStatus = keyof TypeUserStatus;
/* 
type UserStatus = "REGISTERED" | "INACTIVE" | "NOT_FOUND" | "BANNED"
*/
Enter fullscreen mode Exit fullscreen mode

typeof userStatus[keyof typeof userStatus] finally extracts the object values as a union (mapped types
).

const userStatus = {
  REGISTERED: 'registered',
  INACTIVE: 'inactive',
  NOT_FOUND: 'notFound',
  BANNED: 'banned',
} as const;

type TypeUserStatus = typeof userStatus;
type UserStatus = TypeUserStatus[keyof TypeUserStatus];

/*
 type UserStatus = "registered" | "inactive" | "notFound" | "banned"
 */
Enter fullscreen mode Exit fullscreen mode

So the object is the JavaScript (value space) part.

The rest (type space) communicates to TypeScript what that object represents at compile time.

The issue with the plain union

type UserStatus = "registered" | "inactive" | "notFound" | "banned" 
Enter fullscreen mode Exit fullscreen mode

is that the actual string values are used in the code. If for business reasons you need to change to

type UserStatus = "REGISTERED" | "INACTIVE" | "NOT_FOUND" | "BANNED" 
Enter fullscreen mode Exit fullscreen mode

all those values in all the files that use them have to be found and changed (difficult or impossible if you don't control the code that uses the exported values).

With the object using userStatus.NOT_FOUND decouples the code from the raw string value. Just change the userStatus object values

const userStatus = {
  REGISTERED: 'REGISTERED',
  INACTIVE: 'INACTIVE',
  NOT_FOUND: 'NOT_FOUND',
  BANNED: 'BANNED',
} as const;

type TypeUserStatus = typeof userStatus;
type UserStatus = TypeUserStatus[keyof TypeUserStatus];
Enter fullscreen mode Exit fullscreen mode

The issue with TypeScript enums is that they aren't a great fit for JavaScript and were largely inspired by C# enumerations. The ECMAScript Enums proposal doesn't seem to be going anywhere.

The constants-in-an-object approach interoperates with JavaScript with a minimum amount of friction. So while it may be "ugly" it's functional.

TypeScript will eventually deprecate features that don't align with JavaScript. For example namespace (formerly "internal module") seems to be on its way out as there is no JavaScript equivalent (namespacing is often emulated with objects), and ECMAScript private fields could be taking over for private members.

Thread Thread
dvddpl profile image
Davide de Paolis Author

this is awesome

now, const Obj as const make su much more sense. and it's amazingly useful!
thank you so much for sharing such an interesting insight!