4 ms·
Const objects really are better than enums, in every way except declaration brevity. They're erasable syntax, so they work in environments that just strip type
by spankalee 2y ago
Const objects really are better than enums, in every way except declaration brevity.
They're erasable syntax, so they work in environments that just strip types. Their emit is just what you write without the types. They can be composed in a type-safe way with standard JS operations.
You can still write JS docs for values, deprecated the, mark them as internal, etc.
type ValueOf<T> = T[keyof T];
const Foo = {
/**
* A one digit
* @deprecated
*/
one: '1',
two: '2',
three: '3'
} as const;
type Foo = ValueOf<typeof Foo>;
const Bar = {
blue: 'blue',
} as const;
type Bar = ValueOf<typeof Bar>;
// You can union enum objects:
const FooOrBar = {...Foo, ...Bar};
// And get union of their values:
type FooOrBar = ValueOf<typeof FooOrBar>;
const doSomething = (foo: Foo) => {}
// You can reference values just like enums:
doSomething(Foo.two);
// You can also type-safely reference enum values by their
// key name:
doSomething(Foo['two']);
Given the TypeScript team's stance on new non-erasable syntax, I have to think this is how they would have gone if they had `as const` from the beginning. Ron Buckton of the TS team is championing an enum proposal for JS: https://github.com/rbuckton/proposal-enum https://github.com/rbuckton/proposal-enum Hopefully that goes somewhere and improves the declaration side of thigns too.
- hassleblad23 2y agoIts a shame because I like the enum way of declaration a lot more. `const Foo = { Bar: 'bar' } as const` - this just feels a bit weird.
- spankalee 2y agoThat's a taste thing. Personally, I like my TypeScript as a superset of JS with types, so I dislike all the custom value-space syntax. `const Foo = { Bar: 'bar' }` is how I would write an enum-like object in JS, so that's how I want to write it in TypeScript, just with added types.
- DidYaWipe 2y agoYes, why do we have "const" at the beginning AND end?
- disintegrator 2y agoThis is spot on but the issue I called out in my post is that there’s nothing drawing devs to import the mapping. Like there’s the immediate convenience of passing a string literal to a field that’s a string union for instance. You’ve done a nice thing in your snippet and named the mapping and corresponding type the same but that’s also uncommon though I’m seeing it more nowadays. So as I see it it’s very possible to end up with a codebase that inconsistently uses the object mapping if that matters to you.
- spankalee 2y agoI don't know how important it is to bad use of the values directly, but that is also possible. You have to intersect every value with a brand, like: type Enum<T> = { [K in keyof T]: T[K] & {__brand: never}; } const _Foo = { one: '1', two: '2', three: '3' } as const; const Foo = _Foo as Enum<typeof _Foo>; type Foo = ValueOf<typeof Foo>; And now, this will work: doSomething(Foo.two); But this will error: doSomething('2');
- deleted 2y ago[deleted]
- davorak 2y agoUnlike the enum solution this is not nominal to my understanding. const t = ('2' as '2' & {__brand: never}); doSomething(t); Does not trigger an error. So you can do something like const _Foo2 = { two: '2', } as const; const Foo2 = _Foo as Enum<typeof _Foo>; type Foo2 = ValueOf<typeof Foo2>; doSomething(Foo2.two); without triggering a type error too. With built in enums that would trigger an error enum Bar { No = 'No', Yes = 'Yes', } function doSomethingBar(message: Bar): void { } // no type error doSomethingBar(Bar.No); // type error doSomethingBar('No'); enum Bar2 { No = 'No', Yes = 'Yes', } // type error doSomethingBar(Bar2.No);
- spankalee 2y agoYou can always cast your way around nominal typing, even with enums. So you can do: doSomethingBar('No' as Bar); But you can make my Enum<> utility tighter by including the object type in the brand: type Enum<T> = { [K in keyof T]: T[K] & {__brand: T}; } Then, if you had another const object Baz with the same value as Foo, you would get an error here: doSomething(Baz.one); The only time when you wouldn't get an error there is if the whole Baz enum object was assignable to Foo.
- cies 2y agoWhen I see this it makes me want to run for ReasonML/ReScript/Elm/PureScript. Sum types (without payloads on the instances they are effectively enums) should not require a evening filling ceremonial dance event to define. https://reasonml.github.io/ https://reasonml.github.io/ https://rescript-lang.org/ https://rescript-lang.org/ https://elm-lang.org/ https://elm-lang.org/ https://www.purescript.org/ https://www.purescript.org/ (any I forgot?) It's nice that TS is a strict super set of JS... But that's about the only reason TS is nice. Apart from that the "being a strict super set" hampers TS is a million and one ways. To me JS is too broken to fix with a strict super set.
- dkersten 2y ago> (any I forgot?) Gleam has real sum types and can compile to JS. https://gleam.run/ https://gleam.run/
- cies 2y agoIndeed! I thought it was BEAM only...
- spankalee 2y ago> It's nice that TS is a strict super set of JS... But that's about the only reason TS is nice. I mean, yes, exactly?? That's TypeScript's entire reason for being, and it's no small thing. I use TypeScript where I would have used plain JavaScript. If I have a reasonable choice of an entirely different language - ie, I'm not targeting browsers or Node - then I would definitely consider that. I personally haven't seen that any compile-to-JS language is worth the interop tax with browsers or the JS ecosystem, and I've built very complex apps on GWT and used to be on the Dart team working on JS interop.
- Freedom2 2y agoCivet (https://civet.dev https://civet.dev) is probably my favorite one if I want something a bit fancier than Typescript, purely because it shares the same elements that you are as "opt-in" as much as you like, at least in my limited experience.
- fixprix 2y agoTypeScript/ES6 is such a great language with a feature set far ahead of other languages in many ways. The lack of enums though is a sore spot. I really hope that proposal you mentioned can move forward. Also you can improve your implementation with Object.freeze(Foo) and { one: Symbol("1") }
- demurgos 2y agoThe solution that you propose is a great relatively-lightweight solution for enums compatible with `erasableSyntaxOnly`. I see also other comments discussing other solutions which are worth comparing. From my side, I wanted to keep nominal typing and support for lightweight type-level variant syntax (I often use enums as discriminated union tags). Here is what I landed on: const Foo: unique symbol = Symbol("Foo"); const Bar: unique symbol = Symbol("Bar"); const MyEnum = { Foo, Bar, } as const; declare namespace MyEnum { type Foo = typeof MyEnum.Foo; type Bar = typeof MyEnum.Bar; } type MyEnum = typeof MyEnum[keyof typeof MyEnum]; export {MyEnum}; I posted more details in the erasable syntax PR [0]. > This uses `unique symbol` for nominal typing, which requires either a `static readonly` class property or a simple `const`. Using a class prevents you from using `MyEnum` as the union of all variant values, so constants must be used. I then combine it with a type namespace to provide type-level support for `MyEnum.Foo`. > Obviously, this approach is even more inconvenient at the implementation side, but I find it more convenient on the consumer side. The implementer side complexity is less relevant if using codegen. `Symbol` is also skipped in `JSON.stringify` for both keys and values, so if you rely on it then it won't work and you'd need a branded primitive type if you care about nominal typing. I use schema-guided serialization so it's not an issue for me, but it's worth mentioning. > The "record of symbols" approach addresses in the original post: you can annotate in the namespace, or the symbol values. [0]: https://github.com/microsoft/TypeScript/pull/61011#issuecomment-2703776638 https://github.com/microsoft/TypeScript/pull/61011#issuecomm...