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 Hopefully that goes somewhere and improves the declaration side of thigns too.Sum types (without payloads on the instances they are effectively enums) should not require a evening filling ceremonial dance event to define.
(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.
type Color = "red" | "green";
What GP is doing is some scaffolding on top to make the values more discoverable and allow associating arbitrary Color-specific metadata with them.These are dependent types which none of the languages above can enable. Meaning the type system can actually read values in your code and create types from the code. This is not inferring the type, this is very different.
For example:
const PossibleStates = ["test", "me"] as const
type SumTypeFromArray = (typeof PossibleStates)[number]
let x: SumTypeFromArray = "this string triggers a type error as it is neither 'test' nor 'me'"
So in TS you can actually loop through possible states while in ML style languages you would have to pattern match them individually.And on top of that, each of them has a whole new collection of ceremonies you're going to have to learn.
All for what, to avoid `as const`?
Gleam has real sum types and can compile to JS.
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.
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');`const Foo = { Bar: 'bar' } as const` - this just feels a bit weird.
`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.
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#issuecomm...
Also you can improve your implementation with Object.freeze(Foo) and { one: Symbol("1") }
There are certain situations where refactoring a string in a union will not work but refactoring an enum will. I don't want to type strings when, semantically, what I want is a discrete type. I don't even care that they become strings in JS, because I'm using them for the semantic and type benefits, not the benefits that come with the string prototype.
Likewise, enums represent a discrete and unique set. The fact that there is either a number or a string used under the hood is irrelevant.
I imagine using numbers or strings was useful for interop with vanilla JS (where JS needs to call a TS function with an enum as an argument), so it makes sense to use it instead of Symbols, which is what I typically pretend enumd are.
Agree with the author that in almost every other way unions are better though... they play much more nicely with the rest of the type system. I find it endlessly annoying that I have to refer to enum members directly instead of just using literals like you can with union types.
Makes sense. You can emulate that behavior by having an object literal with const assertion AND a union type of the same name derived from the object literal.
(the typeof part is just so you don't repeat yourself, or did you have something else in mind?)
I'll be blunt: at the surface level, it looks like literal unions are something that only someone with an irrational axe to grind against enums would ever suggest as a preferable alternative just to not concede that enums are fine.
If the problem lies in the low-level implementation details of enums, I cannot see any reason why they shouldn't be implemented the same way as literal unions.
So can anyone offer any explanation on why enums should be considered bad but literal unions should be good?
If you’re using a bundler then your’re not to going benefit from it in the medium term. It’s possible this will unlock faster build times with them in the future.
The member documentation point is a good one, I'll look what can be done with my solution.
const Thing {
one: “one”,
two: “two”,
three: “three”
} as const
Or just type Thing = “one” | “two” | “three”
I’ve been thinking of getting rid of the simple string enums I have but it’s not clear to me why one is preferred over the other by people.If you need the actual strings to iterate over or validate against, deriving the value from an const array is helpful:
const THINGS = ['one', 'two', 'three'] as const
type Thing = THINGS[number] Thing.one
or Thing.two
while having each refer to a discrete symbol, you should probably use Symbolsso:
const Thing = {
one: Symbol(),
two: Symbol()
} as const;
will prevent anything equality matching that isn't intentionalBut const enum seems to have several pitfalls. https://www.typescriptlang.org/docs/handbook/enums.html
I think it's because a lot of tooling (excepting TSC) doesn't support cross-file const enums. But I agree - it's one of the reasons I started using TypeScript way back in 2013. I wouldn't be able to write comprehensible performance sensitive code without it.
When targeting javascript, it seems to me that the obvious approach is to use symbols for enums. But symbols have a lot of WET.
(of course, typescript's safety is unfixably broken in numerous other ways, so why bother?)
Whatever that's supposed to mean.
In other words, it's making the strongest version of an argument for the opposing side of the argument. The author doesn't like enums but is talking about their best attributes.
TypeScript sure loves the "our only documentation lives in the changelog" approach to stuff, huh?
- The on-site Algolia search returns 0 results for "erasableSyntaxOnly"
- The blocked-from-search release notes[0] looks like actual documentation - but urges to check out the PR[1] "for more information," despite the PR description being essentially blank.
- The CLI options page[2] describes it thus: "Do not allow runtime constructs that are not part of ECMAScript," with no links to learn more about what that means.
Edit: actually, I take it back! Clicking the flag on the CLI page takes one to an intimidating junk drawer page... but my issues with discoverability stand: https://www.typescriptlang.org/tsconfig/#erasableSyntaxOnly
[0] https://www.typescriptlang.org/docs/handbook/release-notes/t...
[1] https://github.com/microsoft/TypeScript/pull/61011
[2] No idea why on-site search doesn't pick this up: https://www.typescriptlang.org/docs/handbook/compiler-option...