Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Despite the large amount of criticism in the comments here I think that the point the article makes here is pretty valid.

The described features are not what TypeScript itself wants to be, and I think if it wasn't for backwards compatibility the team would remove some of them.

IIRC namespaces as well as the `import = ` syntax come from a time where ESM wasn't a thing yet but a module system was very much needed. So now that ESM can be used pretty much everywhere namespaces can be avoided and therefore reduce the learning surface of TypeScript.

Enums IMO have no advantage over union types with string literals, e.g. `type Status = 'fulfilled' | 'pending' | 'failed'`. They map way better to JSON and are generally easier to understand, while still benefiting from type safety and typo protection.

Well the private keyword is kind of like namespaces in that it came from a time where the feature was needed/wanted, but the EcmaScript spec was not moving fast enough. So they made their own version of it which is now obsolete.

And for decorators, IIRC the decorator spec has already moved on and TypeScript's implementation is no longer up to date with it. And the spec itself is only stage 2 as mentioned in the article, so I wouldn't recommend using decorators either, you will face breaking changes with them some time in the future.

Furthermore, it is far more likely that you run into trouble when using one of these features with a compiler that is not TSC, e.g. esbuild or Babel. Decorators have never been working all that well with the Babel plugin. Enums are probably fine here.



One advantage of enums is that you can iterate over all possible values of an enum, but not a union type.


You can if you derive the union from a const array using indexed types.

  const MyTypeValues = ['a', 'b'] as const;
  type MyType = typeof MyTypeValues[number];
MyType is now a type 'a' | 'b'


So... You have two declarations, one of which is a real array that's allocated at runtime. Is that really better than an enum?

Not to mention, there's a slight mental overhead to parsing this. When I see this code, I might wonder if there's a reason for this to be an array. I might wonder if the order is intentional.

An enum has a more clear intent. My only complaint is that enums are not string-by-default, so we end up writing our variants twice:

    enum MyType {
        A = 'a',
        B = 'b',
    }


>one of which is a real array that's allocated at runtime.

To be clear, the enum is also defined at runtime. So this specifically isn't a difference.


Yes, I didn't intend to imply otherwise, but I could've elaborated.

Some people will argue a preference for string literal union types over enums because the string literal types don't have any runtime overhead. They just provide type safety at write-time and are bare strings at runtime. But as soon as you start adding arrays and custom type predicate functions to work with them, you're adding runtime objects, which removes that particular advantage over enums.


One minor advantage is that you can import the type on its own. If an external app just needs the types, it can import them without affecting its bundle at all.


Fair point.


> Is that really better than an enum?

Substantially. Look at the generated code for an enum.

Also, this approach does not suffer the problems described by the article.


> Substantially. Look at the generated code for an enum.

I'll give you that. It looks like the TS compiler (according to the playground site) spits out some code that's intended for maximum compatibility with older versions of JS, even when targeting newer versions (which makes sense, since nothing is technically wrong about it).

It spits out:

    "use strict";
    var MyType;
    (function (MyType) {
        MyType["A"] = "a";
        MyType["B"] = "b";
    })(MyType || (MyType = {}));
when, we would obviously write the following in modern JS:

    "use strict";
    const MyType = {
        A: "a",
        B: "b",
    };
So that's a bit disappointing.

So, this could matter if you intend to actually read the emitted JS. If, however, you're TypeScript-only, this is more-or-less the same as reading the ASM spit out by your C compiler or the Java bytecode spit out by javac.

> Also, this approach does not suffer the problems described by the article.

This argument doesn't hold water, unless you're taking a philosophical stance. The argument is that most TypeScript features don't actually spit out JavaScript code and this one does.

But, if you're going to write an array that lists your variants (and then write code elsewhere to check if a string is contained by said array, etc), then "extra" JavaScript code is still being generated- it's just generated by you instead of the TypeScript compiler. Why should we care who generates the code?

This argument only works when we're comparing to writing a string literal union type and no other supporting code for that type. My comment was specifically addressing the case of writing an array to hold our literals instead of writing an enum, and I stand by my claim that an enum is better because it's the same runtime overhead, but more clearly communicates intent/semantics to your fellow TypeScript devs (including future-you).


The modern Javascript version of that would be the same as the compiled Typescript version, the const version doesn't do the same things.

The IIFE is creating an object only if it doesn't already exist, and adds "A" and "B" to it. "var" doesn't error on a redeclaration, so if MyType already existed you'll get a mashup of the two versions of it. Even if the const was switched to var in the second one, that would still be a straight replacement of the values instead of merging them.

I haven't used Typescript, but I imagine this style was used so enums could gain new values later in the code without having to worry about execution order.


Oh, good point. I forgot about var not getting mad at redeclaration. So the default TS implementation will merge an existing object with the newly defined fields. I'm too lazy to check, but I wonder what happens if MyType already exists and is a scalar, like a number...

I think I was still accidentally correct in saying that's what we'd write because who the hell actually WANTS the default behavior? :p


> > Also, this approach does not suffer the problems described by the article. > > This argument doesn't hold water, unless you're taking a philosophical stance.

Respectfully, philosophy has nothing to do with this.

The argument that the other person made does, in fact, hold significant water. There are extremely long discussions about it on the Typescript GH repo.

.

> The argument is that most TypeScript features don't actually spit out JavaScript code and this one does.

No, it isn't.

.

> then "extra" JavaScript code is still being generated

I never said extra code was a problem. I have no problem with this.

What I said was that I found the code emitted by the enumeration stack to be problematic. You seem to have inferred cause (incorrectly.)

.

> Why should we care who generates the code?

Do you believe that I think a compiler should not generate code?

I never said anything of that form.

Genuinely, it's difficult to hold a discussion with people who read so deeply between the lines that they come to bizarre conclusions, then think those conclusions belong to the person on the other end of the wire.

.

> This argument only works when we're comparing to writing a string literal union type and no other supporting code for that type.

You're not talking about the same argument that I am.

.

> but more clearly communicates intent/semantics to your fellow TypeScript devs (including future-you).

I write documentation.


Your comment said two things. You said that you don't like the generated implementation for enums and then you said "this approach does not suffer the problems described by the article."

The article listed literally one reason to not use enums, and that reason is because it requires to compiler to produce JavaScript code. So, if that's not what you're talking about, then I have no idea what "problems described by the article" you could possibly be talking about.

With all of your complaining about my response, you still didn't explain it.


This is the way, because iterating enums produces odd results due to a bidirectional mapping.

I had always used enums in TS until this year, but union literals are better.

I create my own enums with const objects, compute the type based off the object's values. So very similar this, just with an object as the source instead of an array.


Iterating over an enum in TypeScript always felt like code smell to me because of the filtering code I'd have to write to deal with the bidirectional mapping.


I often use that approach but (especially when you're importing from a seperate package) the compiler will sometimes view MyType as just an alias for string and won't catch typos.

Am I missing something in how to use this?


`as const` is the important bit there, see this playground link https://www.typescriptlang.org/play?#code/MYewdgzgLgBKlQIICd...


> Enums IMO have no advantage over union types with string literals, e.g. `type Status = 'fulfilled' | 'pending' | 'failed'`.

Well I would say having to not repeat and update your code everywhere when you change or add a possible value is a pretty big advantage.


In my experience, even if I do change an enum value I also want to change the name in code. Having enum { complete = 'finished' is a little weird so I just change it to enum { finished = 'finished' } anyway.


My IDE does this just fine (WebStorm, btw)


But the peer who reviews your code will hate it.


No, it was a team choice.


Ok, that's good.


Numeric enums are a lot more useful than string enums, in my view.


Just don't use numeric enums with implicit values. If you add a member anywhere but the end of the enum, it'll change existing members' values


I like string enums, since they are self-documenting, I guess int enums are smaller when sent over network. Could you expand on that?


In the use cases where I most typically use enums, I don't want to think about the question of runtime representation; I just want a set of arbitrary symbols that are different from one another. Implicitly initialized numeric enums do this idiomatically and concisely.


If you truly don't care about the runtime representation, then it sounds the idiomatic JS/TS construct you actually want is Symbol()

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

https://www.typescriptlang.org/docs/handbook/2/everyday-type...


Symbol is an extremely heavy hammer. It's also difficult to use correctly.

What value do you think it offers here?


I brought up symbols because ameliaquining's declared requirements match symbols to a T:

> I don't want to think about the question of runtime representation; I just want a set of arbitrary symbols that are different from one another.

I wouldn't know how such requirements translate into value for her. Personally, when I need a set of type-checked fixed values, I favour string literal unions because a readable runtime representation eventually comes in handy.

The only time I recall using symbols was in order to have a "newtype"-like construct, which is a different thing entirely.


I agree that it might have been better for that to be the TypeScript idiom, but it's not, due to the longer-standing popularity and concision and language-level support for enums. (Which couldn't have originally been designed to be lowered to symbols, because at the time symbols weren't widely-supported enough.)


Symbols are very well supported in TS today. You even get proper type enforcement and intellisense when using symbols as the names of methods and properties, for instance. Whether you should use them for enum-like purposes or not has more to do with how you want to use the values though. For instance using symbols for defining numeric protocol values doesn't make sense. Same for HTTP methods, where you are always ultimately passing a string down to the http client/server interfaces


Good point, I'm mostly doing web stuff so I always think about how something looks in JSON in the network panel of my browser.

If you just use the value within your code the runtime representation does not matter, you're completely right.


Why do you find them self documenting?

Usually it's just

    enum Method {
      Get = "GET"
      // ...
    }
I usually always put doc comments on enums and their variants.

Regarding why numeric ones are (only sometimes) more useful than string values: bit flags comes to mind, faster comparison of numbers than strings (not always tho... unless this is a misconception, but I don't believe so), you mentioned smaller bundle size already.

As for "auto" enums: the fact they're numbers now is an implementation detail. They could be unique Symbols in a future versions of typescript. You can do that manually now too, but I'm talking about the automatic (without assignment) syntax.

Regarding article: I... Half-agree. But I'd not completely disregard/avoid enums. At the very least, they can be useful to help model some behaviours/states in more readable, accessible, and coherent way (Other comments went into a more in-depth defence of enums, thought).


   enum Method {
      Get = "method/GET"
      // ...
    }
The string value can be very helpful for the reader - be it a human or log ingestor


Could you expand on this? I can't think of places where a string enum would be less useful than a numeric, although I can think of places where numeric enums fall over, e.g. if you have a value that can be one of two different enums and you have to figure out which it is - runtime numeric enum values shouldn't be compared because they regularly have the same value.


Though I have never seen anyone till now call TypeORM a fine choice.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: