tldr: avoid features in TypeScript which must emit runtime code when compiling to JavaScript - that is, any TypeScript-exclusive feature that’s not type annotations.
Honestly most of these features are really useful, and as someone who never wants to work in raw JavaScript, I wish they were just added to JavaScript instead. Why shouldn’t JavaScript have enums, namespaces, private modifiers which aren’t #, and decorators? JS already gets new features which break backward-compatibility, mine as well add features which fix compatibility with TypeScript.
I completely disagree with that article. Instead of "avoid this or that", one should be understanding how Typescript compiler works and not treat it as a blackbox. Ultimately, Typescript does compile to Javascript.
One cannot use Typescript without understanding Javascript, since every single Javascript library is not written in Typescript.
One should also understand the machine code that a traditional language compiles to and how the CPU executes it. Ultimately, your language compiles to machine code. /s
No one should need or worry about what an abstraction compiles to unless they’re specifically working on that abstraction or there’s a language bug.
Typescript compiler is first and foremost a type system for Javascript. It's not an independent language. In fact it broke backward compatibility many times to follow the ES spec.
> One should also understand the machine code that a traditional language compiles to and how the CPU executes it. Ultimately, your language compiles to machine code. /s
If libraries you use are all written in machine code, then sure, you should have an understanding of machine code. Your comparison clearly doesn't work here.
> No one should need or worry about what an abstraction compiles to unless they’re specifically working on that abstraction or there’s a language bug.
When an abstraction is that leaky, it's barely an abstraction. Typescript does force you to choose a Javascript version as a compilation target. Obviously you are forced to know what Javascript version supports what feature because Typescript isn't going to polyfill every missing feature depending on your Ecmascript target.
This is how ES6 got classes. Developers wanted JS to be some other language they favored more. In that case specifically people were really hoping to make the language look and feel like Java, probably because they were trained in Java and couldn’t figure out functions as first class citizens.
I think class were needed because too many developers were creating their own (incompatible) class systems on top of prototypal inheritance (which is more verbose).
The problem with Typescript is that too many Typescript developers don't understand Javascript itself, which is an completely different issue. That and the obsession for some to reproduce JEE everywhere including in the browser...
In my humble opinion, they're both fine. Use functions where you need to, and do the same with classes. They're both citizens of the language, so why not utilize them?
> But before that there were countless competing models for creating objects or object factories.
Java and C# had object factories even though in those languages classes could not be avoided. People wanted classes because they could not figure how to program without them.
> To pretend…
Don’t use this or new in your code and suddenly a tremendous amount of your code is exposed as unnecessary superfluous vanity. That isn’t making the language into something else.
I'd go further. There's no strong argument for why code-gen is bad. Why is a compilation process that is simply "remove type annotations" inherently better than one that emits code, or does code transformations, other than just personal preference, aesthetics, or simplicity?
About the only positive that is qualitatively different is the simplicity of comparing the output to the input. But for someone building a large typescript codebase, and who uses sourcemaps, it's not really a big issue. There are many many languages that compile-to-JS, and I feel that insisting on 'purity' for purity's sake isn't really a good justification. That, TS as a super-set of JS instead of as a isomorphic mapping between constructs is a perfectly viable way to innovate in the language space.
I think TypeScript loosely mapping to JS is important if just because JS sourcemaps suck, like they're so often randomly ignored in callstacks etc. And JS semantics are so subtle it makes transpiling any other language a pain.
But this doesn't justify removing every codegen feature. Namespaces and enums won't make your code less reasonable, and moreover they're just syntax sugar, they don't change actual JS semantics.
I see the next to zero codegen in typescript as a strategy: it removes all discutions about languages features besides typing, guarantees next to zero issues in production in case of code generation bug, making compiler deliveries safe and avoid need of coordination in toolchain.
Enums and namespaces are hardly complex code gen or generate 'issues'. People would often manually namespace in JS a few years ago, and namespaces and enums can really just be seen a lightweight holder object instances. Indeed, enums in Java are class instances.
Besides, there is an straightforward way to remove enums from a program just like removing type annotations: Inline them as static fields of an object.
There is a simple syntactic transformation.
e.g. change 'enum' to 'const', add a '=' before the '{'
and use ':' instead of '='
const HttpMethod = {
Get: 'GET',
Post: 'POST'
};
// now this no longer breaks
const method = HttpMethod.Post;
Namespaces can be translated in almost the exactly same way.
Trouble with your const there as written is that you don’t have a type that’s equal to "GET" | "POST". Fortunately, this can be done without repetition or too much bother:
const HttpMethod = {
Get: 'GET',
Post: 'POST',
} as const;
type HttpMethod = (typeof HttpMethod)[keyof typeof HttpMethod];
Maybe wrap the `{…} as const` in Object.freeze(…) for good measure.
It’d be really nice if they’d improve the ergonomics on this in some way (`type Values<T> = T[keyof T]` would reduce it to `type HttpMethod = Values<typeof HttpMethod>`, which is a start but not enough), to make it a genuine and suitable alternative to enum (minus the other frippery that’s generated) and const enum (because it’s pure JavaScript, not an extension).
What is there left to use in the end? Type annotations?
Maybe recommend not to use Typescript altogether then. Their only reasoning for this seems to be that the features "be more likely to break when using build tools other than the official TypeScript compiler".
Their reasoning is missing the forest for the trees. The point is that most TS features do not map easily to JS, making any interaction with the resulting code a pain. That includes any static analysis of the JS output (not all tools support TS, after all), debugging both with step by step and console logs, and monitoring in prod.
> What is there left to use in the end? Type annotations?
I've always used TS for the types and nothing more, and been happy with it. Seems to me that most of the JS community has come to adopt this approach over time, and I don't see what's bad about it.
Type annotations are the whole point of TypeScript. The rest is mostly distraction and historical decisions that made sense at the time but have aged poorly.
> What is there left to use in the end? Type annotations?
That's what Typescript mostly is, that's where its success comes from. Type annotations for existing JS code. The vast majority of people need to type existing JS code, and compile to JS. A few minority need some specifics from the TS type system. Outside of that, there are other options.
I've never had any issues with enums, namespaces or private keywords (can't speak for decorators as I haven't used them yet). Although I do understand the reasoning for using the '#somePrivateField' rather than 'private somePrivateField'.
But features like enums are part of the reason why we even have tools like TypeScript, because JavaScript lacks these features.
I wish TypeScript somehow would use #-private fields underwater when compiling code. `#field` is pretty confusing; I keep thinking it's a comment and it looks ugly
Although I do understand the reasoning in the article, I completely agree with you.
Switching between Python and TypeScript/JavaScript all the time, using ‘#’ to define private field feels weird and I personally prefer more explicit way of writing code. Plus AFAIK the ‘private someField’ is common in other languages (Java, Scala,…).
My problem with enums isn’t so much that it breaks type-level extension, it’s that it breaks structural typing.
I don’t particularly care what the generated JS looks like when I do all my work in TS, but I do care that the type system of the language I do work in works in a consistent manner.
I find string enums useful when I want nominal typing. However, I do think that it is a little confusing when most of the type system is structural, except for string enums. What complicates things further is that any number can be assigned to a number enum without casting, which I understand is so enums can represent bitfields (e.g. so Permissions.Read | Permissions.Write is still of type Permissions instead of number), but I wish numeric enums worked the same as string enums and doing bitwise operations on these enums would return the enum type and not a type-erased `number`.
I mostly agree with the rest of the article’s recommendations, but don’t fully agree with their reasoning.
- Yes, namespaces should be avoided, but not because they generate extra code. Namespaces are not recommended anymore and modules (actual JS modules not TS’ `module`) is the recommended approach now.
- Yep, private fields are better for ensuring a field is truly private.
- I think decorators are fine to use if you’re prepared for your code to potentially break in the future if they’re standardised. Developers use new/unstable features all the time (e.g. stage 0/1 JS proposals via Babel, nightly Rust toolchain). And I don’t believe the lack of standardisation of decorators should be a factor in deciding whether to use a library that requires these.
I think you may have misunderstood the complaint, which is perfectly valid. A few paragraphs earlier:
> The downside to enums comes from how they fit into the TypeScript language. TypeScript is supposed to be JavaScript, but with static type features added. If we remove all of the types from TypeScript code, what's left should be valid JavaScript code. The formal word used in the TypeScript documentation is "type-level extension": most TypeScript features are type-level extensions to JavaScript, and they don't affect the code's runtime behavior.
And:
> Most TypeScript features work in this way, following the type-level extension rule. To get JavaScript code, the compiler simply removes the type annotations.
> Unfortunately, enums break this rule.
And then there is an explanation about why this is important: it makes life hard for tooling, especially fast tooling; for in the absence of such features, JavaScript tooling can support TypeScript with little bother, just dropping the TypeScript bits and getting equivalent JavaScript; but in the presence of such features, they have to either use tsc (slow!) or implement more TypeScript-specific stuff.
Compilation of most TypeScript features to JavaScript simply removes the TypeScript bits. Enums, however, have to be transformed, adding to the output JavaScript something that was not in the source JavaScript.
It doesn't support one variant of them, const enums for example. That ties you to tsc emit. Its pretty clear that if the tsc team could they would remove enums and favour literal unions.
I've just looked this up and it seems to support `const enum` just fine[0]. I remember Babel not being able to process `const enum`, since it goes across module boundaries and Babel does not.
Not saying I agree, but this is a completely valid viewpoint.
When I initially saw Typescript, I thought the point was to add features from strongly-typed languages and then transpile into Javascript. (IE, a more modern version of GWT, a Java to Javascript transpiler.)
The point of the article, though, is that Typescript works best when its extensions to the language can simply be dropped. That's clearly a "we've worked with this for many years and this is a big lesson from experience" statement, so I wouldn't discount it.
Typescript works fine with it's extensions to the language. In fact, I can't imagine how it would not work fine - that would be a serious issue in the official compiler or tools.
It's the other tools that author is/was using that are having issues. It's silly to provide blanket statements about very useful features like enums or namespaces just because some third-party tool is struggling with them IMO.
I recently picked up TypeScript and I sort of agree with the author. It’s just so much cleaner to use Types instead of enums. That being said, I don’t think enums are bad if there is a good reason to use them. Checking if something is a type of X isn’t one such thing in my opinion, but that’s probably religion.
Namespaces mKe no sense to me. It’s probably because Microsoft drives TypeScript, but even though I was a C# developer for 10 years before moving on, they’ve just always been terrible to me. Their functionality is the sort of thing that is nice in theory, but really terrible in real world projects that run for years with variously skilled developers in a hurry.
Private is silly to me, but this is mostly because classes are silly to me. I can see why you’d want it if you use a lot of classes, I just don’t see why you would do that unless you’re trying to code C# in TypeScript. One of the things I loved the most about switching from C# to Python was how easy it was to use dictionaries and how powerful they were. The combination of TypeScript interfaces, Types and maps is the same brilliance with type safety. But once again, it’s sort of the thing where classes sometimes make sense, and when they do, so might private.
I've been using the method I outlined here [0] and it's not tedious to work with at all. That said you might have a different usecase where my example doesn't work.
What I like about TypeScript is that all these features are optional, declaring that they should be avoided is going too far, bus so would be saying that you should always use all features of a language if it could apply.
Everything has its place, and for allot of the features of TypeScript I think they are designed to be useful when you have a large number of developers working on incredibly large codebases.
I suppose in some ways it's like C++, you can decide to fully embrace all of its features, or code in a much more C like way, just taking advantage of classes.
It comes down to personal/team preference, what works for you.
Personally with TypeScript I'm inclined to code in a "closer to JavaScript" way (but taking full advantage of types obviously), but would happily work in whatever style was prevailing in the project.
I disagree with this. We have a reasonably large Angular application that is only going to get much bigger (hard to define what that means ... big telephony app with tens of thousands of customers). I am the lead and architect.
We use enums and private keywords. With the private keywords, all I care about is that it is logically correct. We use private when things are truly private, i.e. they are only called from within the same class and don't need to be made visible to the view template or outside of the class. I honestly don't care what this transpiles down to, the point for us at least is not to make things "truly private" (good luck with that in JavaScript). It's simply to compiler-enforce rules. We also have ESLint to ensure that our private fields are all below the public ones to keep things nice and neat.
I also enforce that we actually make things as public although that isn't needed, and I enforce returning void.
So instead of:
someMethod() {}
I have us use:
public someMethod(): void {}
Just to state what you intend.
I realize "I don't care what this transpiles down to" might really irk some people, but I really don't. In our C# back-end I am much more strict about this stuff, but in JS at the moment given the standardization of the #private fields and the fact I consider them really ugly, I honestly don't care. Just give me a clean code base that enforces we can't reference private fields and methods from our templates.
For enums, I recently wrote a method that does exactly what this article says not to do, use it for GET, POST, and PUT.
What would be a cleaner way to write this? If it has to be refactored into something much "uglier" I don't think I'd prefer it.
This is a service I wrote that handles real-time progress (i.e. show an accurate progress bar when downloading files). I think this is clean and logical.
> I honestly don't care what this transpiles down to, the point for us at least is not to make things "truly private" (good luck with that in JavaScript). It's simply to compiler-enforce rules.
It’s not just JavaScript. With reflection in C# and Java, you can mess around with private variables from outside the classes. For Java, this can have some pretty interesting results, such as 2+2 being equal to 5.[0] The whole point of compiler level annotations is to keep good programmers honest.
If some devious JavaScript developer wants to ruin your library, that's their fault.
Agreed, with reflection you can get around that also, but in JS it's even less of a "real thing" at the moment and I don't see that changing until the far future where we can drop some of this legacy cruft.
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.
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).
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.
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
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.
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.
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.
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:
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.
> 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).
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).
> > 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).
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.
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
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.
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.
> It may be a bit annoying to create many small files, but modules have the same fundamental functionality as namespaces without the potential downsides.
Do they?
It seems to me that namespaces are more powerful and convenient than JS modules as they enable more structure.
I have only dabbled in TS and am not sure how useful they are there. But I assume they would be similar to PHP/C#/Clojure namespaces on a conceptual level.
Yes, as it should be. There are no enums. Instead, you can pass in any of the documented strings (or whatever the base type is). Even inside TypeScript, an enum is really just a union type on steroids.
When you expose TypeScript code to JavaScript consumers you absolutely must validate all incoming data anyway, whatever type you declare.
This is not about trusting the information your users provide. It is about making your APIs accessible to users. If you hide some information, your API is less accessible.
But it's just as hidden either way. The API consumer does not know there's an enum that's supposed to go in there. The only way to get that information is to read the documentation.
I have no idea where you are coming from unfortunately.
Generally speaking, a Javascript user has access to the same type script language features (including hints, auto-completion,...) as a typescript user.
Even the most primitive text editor supports language server protocol nowadays.
The library your interoperating with does not even have to be written in typescript if the author provided @typedef JSDocs.
const enums break this because no value of this type exists
Yes, Nest uses decorators and the reflect-metadata package to get information about them dynamically. Annotations/decorators in other languages are fine, but in JavaScript/Typescript I feel the risk of using them as a dead end feature. I'd like the proposal as much progress as possible, but it's been quiet. In the meantime, a Typescript Nest like system without annotations could be better.
What? This is IMO bad advice. Having a sum type is quite handy for general type-checking, at least insofar as the type truly is an enumerated type (i.e. all possible values are known at design-time). There have been times when TypeScript enums have been indispensable to me when declaring the external interface to some client-facing API. Whatever the API boundary is, a sum type is useful.
Also, TypeScript gives general intersection types, which is quite rare among its peers. (What I wouldn't give some days for mypy to have intersection types, or bivariant functions, or conditional types, or ...)
The only other impetus for this post I can imagine is some weird desire to see typescript as a strictly separate layer above JavaScript that "could be removed" if we wanted it to be. I suppose that was the project's original telos, but today the abstraction is leaky in a few places. I'm a world where JSX is common and radically departs from what would be considered normal JS, I don't see a problem with TypeScript being leaky here and there. Hell, I'd prefer TS to be leakier and add opt-in runtime checking (i.e. code gen), because it would make my life easier in certain instances.
Based on how two of the four points key into it, I think the author is stuck using a tool in their tool chain that doesn't support TypeScript and has been bitten by that tool doing something Byzantine in response to TypeScript-generated code.
That's a problem a lot of developers won't have. I've covered a lot of ground without ever finding a tool that either hasn't been retrofit to support TypeScript or that has issues that are tickled by TypeScript generated code. My blunt recommendation if somebody hits that problem is to find a better tool.
The TypeScript ecosystem is bigger than just the official compiler, though. Many projects use alternative compilers like esbuild or @babel/preset-typescript for their main pipeline and only use the official TypeScript compiler as a typechecker (with `--noEmit`, during CI).
It's true that enums are harder to support for alternative compilers. Especially `const enum` seems to be harder.
> It's true that enums are harder to support for alternative compilers.
As long as it is supported — and at least esbuild does, who cares (as a user). Should I start avoiding every JS feature that is hard to implement in alternative JS runtimes?
yes, we should also restrict every C++ feature to what Borland Turbo C++ 3 supports too, since there are still school that teach that making it part of the C++ ecosystem.
Actually, as Amish communities are part of the human ecosystem, maybe we should also restrict anything we build in the real world to something that can be useful to the Amish and stop building anything requiring a power grid ?
In some cases you can use it to define string arguments to an external library. Maybe table names to an ORM or a well-known file path. This helps because you get autocomplete from typing `MyEnum.` and seeing options, even though the external function takes a plain string.
It's not possible to get the autocomplete unless the function/method you're calling takes a union. Many libraries can't do that because they need to be flexible in what they receive. So yeah, you can define a union, but editors don't have the context to know your union applies to the call.
If you have to change one of the underlying values, you only need to change it in one place. Of course, you could just use constants, but then you’ll just have a const enum with extra steps.
Unfortunately, neither (string) enums nor string literal unions are supersets of each other when it comes to functionality. But, IMO, string-only enums are generally better.
You can't test if a given string is an element of a union without writing a custom helper and/or allocating a runtime array of the values.
On the other hand, if you don't need to test unknown strings, then union types disappear at compile time, whereas enums are compiled to real, runtime, objects.
Enums don't look like naked strings in the code. Frankly, it's just nicer on my brain to see `return Color.Red` than `return "red"` and wonder if "red" is just some random text or if it has semantic meaning. Hopefully your IDE is smart enough to take you to where "red" is defined as part of a union type when you want to see other options. Granted- the most popular editors ARE smart enough to do that, but that doesn't help when just reading code with my eyes instead of my hands, or in patches/diffs.
In a TS codebase I'm currently working on, we have the policy of never using "plain" TS enums. Instead, we have a tool that generates our own enum objects using a schema and a generator script for the cases where we want "rich" enums. For other use cases, we use string unions a lot (in combination with a helper function that allows exhaustive matching).
The author misses I think an important point regarding enums: using a union type instead means you can't iterate the list of valid values without restating them in the value domain in a non-DRY fashion. The reason enum generates code is that it is both a type construct and a value construct... That ends up being useful in myriad contexts.
I disagree with the point about enums, they can be used to alias otherwise inconsistent long strings with more readable, consistent, succinct ones. For instance, product SKUs. I've had absolutely no problems with them with either past or current tooling (e.g. esbuild).
They can also be used in the more traditional form to represent some arbitrary values.
Const enums specifically are great, since they disappear on compile you can use them to internally document strings/numbers with verbose names/comments without the cost of a const variable that would appear after compilation.
As for the fact that types cannot simply be stripped out, I've found building using plain tsc and have the bundler target tsc's output directory. This separation is needed since most tools don't support TypeScript project references anyway which I find extremely useful for organizing internal modules.
This whole thing feels basically grounded in purity over practicality. In general it's a good idea to write idiomatic TypeScript. Even when I agree with the given recommendations, the given reasons don't seem like the strongest ones.
I most strongly disagree with the recommendation against enums. Realistically, you will probably never run into a compiler bug from enum emit; maybe something like this might happen with a very complicated runtime feature, but enum emit is dead-simple and hard to get wrong (at least if your toolchain has any tests at all, which it presumably should). And they're generally convenient and fill a useful niche, especially numeric enums with implicitly assigned values. (I'm also curious what the article's authors think of const enums.)
Namespaces have been soft-deprecated, modules are pretty much just better, and so I quite agree that you shouldn't use them, though I'm not sure the risk of compiler bugs is the most compelling argument against. (It is more compelling than with enums, since the required code transformations are much less trivial.)
Decorators, especially with metadata, facilitate lots of useful things that otherwise just aren't possible in TypeScript. It's also the case (though the authors seem unaware of this) that they will never be standardized in the current form that TypeScript has them, because they were based on an earlier version of the design that has since been pretty explicitly rejected. The risk isn't that decorators are never standardized; if that happens then TypeScript will just keep the current design forever and things will be mostly fine. The risk is that they get standardized in an incompatible form and then you have an interesting migration ahead of you. TC39 won't do this lightly, but no one knows exactly what the future holds. So it is a tradeoff to think carefully about, though in the end reasonable people will disagree.
# vs. private is mostly a matter of style/taste, with two exceptions. First, if you have any code on your page that you don't trust not to be doing weird things, strongly consider #, since it provides protection against corruption of internal state. Second, if you have to support older browsers that don't support #, then don't use it; the compiler can downlevel it, but only at significant code-size and performance cost that you don't want to pay (and debugging it in the browser will also be annoying).
Do the authors also disfavor parameter properties? Those also require emit beyond just stripping types, but are super-convenient and useful and don't really conceptually complicate things.
Incidentally, the feature at the top of my own list of "TypeScript features to avoid" (other than namespaces and other soft-deprecated features) is something entirely different: conditional types. Most other advanced type-system features behave reasonably predictably most of the time, but conditional types are very demanding of maintainers' knowledge of fiddly type-system details. I'm not saying it's never worth it (and in particular the built-in utility types are usually fine even though they're conditional under the hood), but whenever possible I try to reach for something else.
> you will probably never run into a compiler bug from enum emit;
That's true, but diagnosing other bugs is an absolute pain in the butt when your enum value at runtime is 0, 1, or 2. You get all of the readability of C with none of the performance :)
Is there really such a thing? Everyone seems to be writing TS with the "fake it until you make it" mantra, never quite reaching the "make it" phase. People still use "interface" and "type" interchangeably without rhyme or reason. Or "import" vs "import type". No one knows what they are doing in TS. Or why. Just look at this entire comment section.
For people that don’t see the problem and are happily using these features, here’s an explanation of the second problem with these sorts of features, additional to the “it’s not just JavaScript with types which is what the label said” reason which is the focus of the article.
The real trouble occurs when TypeScript implements something because that looks like the way things are heading, but then they don’t head that way, and JavaScript and TypeScript diverge incompatibly. The type stuff is generally fairly safe, and fundamentally necessary, but some of the other stuff they’ve added isn’t safe and isn’t… as necessary, at least. Decorators are the current prime case of this divergence problem: they added a feature because it was useful, lots of people wanted it, and that was what people expected JavaScript to get before long, and some were using already through a Babel extension but people were sad about having to choose between nifty features (Babel) and types (TypeScript); and then because they’d added something not in JavaScript, why not go a bit further? and so reflection metadata came along; and then… oh, turns out decorators are actually heading in a completely different and extremely incompatible direction in TC39 now, but people are depending on our old way and PANIC! It’s been a whole lot of bother that will continue to cause even more trouble, especially when they try to switch over to the new, if it gets stabilised—that’s going to be an extremely painful disaster for many projects, because “experimental” or not, it’s a widely-used feature of TypeScript.
This is not the only such case; there’s one other that caused a lot of bother comparatively recently, but I can’t think what it was (I don’t work in TypeScript much).
Sciter used what was loosely a fork of JavaScript when it started, and diverged, for quite decent reasons in some cases I will admit, but this divergence caused more and more trouble, until recently they gave up and switched to JavaScript, … except with a couple of incompatible deviations already and more on the table as probabilities. Sigh. I had hoped a lesson had been learned.
So yeah, I’m content to call these things misfeatures. TypeScript overstepped its bounds for reasons that seemed good at the time, and may even have been important for social reasons at the time, but you’re better to avoid these features.
Did the other feature have anything to do with modules? If so, I can't really blame TypeScript too much for that, since the JavaScript ecosystem is pretty fragmented there and TypeScript has to support all the different things that are in use with a reasonable interop story. Build tools that work purely with JavaScript source code also have to deal with this problem.
Otherwise I'm not aware of any cases besides decorators where TypeScript did something that was incompatible with a later ECMAScript development.
I really don’t remember, sorry. I just scanned through https://github.com/Microsoft/TypeScript/wiki/Breaking-Change... and nothing jogged my memory; I’m wondering if it actually was something to do with decorators, though I still think probably not. I investigated a bit at the time because I was looking into using TypeScript on a certain project and the changes would probably affect me, but I wasn’t actually using TypeScript at the time, and it didn’t stick in my memory. Something about some change being spread over a few versions with deprecation followed by a silent breaking change in the generated code, but I can’t remember what.
I recently listened to an interview with Igor, Angular’s inventor.
In it he talked about how Angular 2 pushed decorators in to TS. And to this day, Angular is the only major JS thing that I can think of that uses decorators.
Creating that ng-abomination was not enough. No. Google also had to poison a perfectly fine language.
Article makes no real good arguments about not using the private keyword. It's more descriptive and carries knowledge from other languages compared to putting a hashtag in front of a variable name.
I agree. Just because JS has its way of doing it does not mean you need to stick to that. I mean, TS adds a bunch of new keywords to the language that JS lacks. Why not simply stick to those intuitive keywords rather than this weird variable naming nonsense that JS has to use because it doesn't have access modifiers of any kind?
The arguments against the four language features in this article all boil down to it not being Javascript. If I wanted to write in Javascript, I'd use files with a .js extension. If you've picked Typescript, I don't think you should worry about whether your code is also valid Javascript "if you remove all the type information" (who's going to do that anyway.)
Unless you're a solo developer, what's more important it to agree on languages and conventions that apply to your team's projects. Once that's done, any change to the team's work process should be a conscious, measured decision.
Note that I also used CoffeeScript 10ish years ago and still consider it to be a superior experience to working in plain-old Javascript.
I continue to maintain that despite all the nice features from ES6 onwards working in straight JavaScript is psycho shit. It’s the NFTs of development.
I’m genuinely looking forward to the day where we have other viable options for the DOM. I see TypeScript at best as a temporary band aid because you’re still stuck in the god awful NPM ecosystem at the end of the day.
I disagree with almost all of this, but I will say have you checked out Deno? It is in fact Typescript without the NPM ecosystem (amongst many other interesting aspects)
Honestly, I’m more waiting for WASM garbage collection to officially land as a standard.
It won’t get me DOM level access but I can at least move a lot of my code there. I’m also quietly hopeful that canvas based rendering can make some huge improvements in the next few years so it doesn’t feel like Flash 2.0 but I’m ready to at least start thinking about letting go of the DOM as the thing I have to care about.
Until then I’m having a good time with Lit (lit.dev) for building web apps that need to be super snappy and “web feeling” which is still basically every customer facing thing.
But in a dream scenario I would way rather be writing apps in Flutter which was at least built from the ground up for building complex user interfaces in a sensible way, but that whole ecosystem is still in some very early days on the web and isn’t a good choice right now for most things, hoping that changes in a few years as they also seem to be targeting the WASM + Canvas path and the web as a platform isn’t there on that yet and neither are they.
There are a few big problems with using Canvas for UI on the web. First and foremost is accessibility- there is no way for your app to convey the information screen readers are able to get from analyzing the DOM along with the ARIA metadata that you (should) put into your markup. Furthermore, users who have trouble using a mouse can use the keyboard on the web, and it usually works very well since the browser handles it and the browser has been battle tested. You would need to implement your own keyboard handling scheme (though I'm certain a ton of apps just wouldn't bother). What about scrolling with touch input? You would have to implement that too and good luck making it as smooth and performant as the system's own native scrolling (let alone making it use the appropriate rubber banding- iOS and Android have separate ways of doing this because Apple patented the original iOS rubber band scrolling)
Secondly, there is no way for automated agents to extract content from your user interface. This includes search engines, browser extensions, the browser itself, or your end users. I think that goes against what the web is, and I hope other devs agree. The mutability of HTML (and thus the DOM that represents it at runtime) is a strength, not a weakness
This was my first reaction also and I think it’s understandable but having looked at proposed solutions to it as well it actually doesn’t seem like such a big deal.
I apologise if I get some minor details wrong here as I am doing this on a phone and recalling this from memory because I don’t have the time to grab the sources right now.
However… the short version of the plan to solve this that I seem to recall to this:
The Flutter team specifically seemed to indicate that they are already used to operating in non DOM environments where they have to support accessibility across Android, iOS, MacOS, Linux and Windows that doing it on web actually isn’t that big a deal as it first seems.
They already have all of the code in place that builds a full tree (like the DOM) which does a complete mapping between every element (widgets in Flutter lingo) on the canvas to their respective bits of accessibility info. They then just take that tree and hook it up to the respective accessibility APIs that each platform exposes.
At no point have they indicated that this looked like it was going to be a serious roadblock or challenge for them. I believe them and they have a history of this approach working elsewhere.
There is just too much money riding on this investment for them not to get accessibility 100% correct in a web native way.
From there if you are able to expose everything through accessibility APIs then you presumably also have everything you need as a search engine or an adblocker to actively read and modify that canvas. At this point the entire argument that it’s just an opaque set of pixels seems to vanish for me.
This is also AFAIK already a solved problem for them in other products where they are using canvas based rendering such as Google Earth and Google Docs.
I don’t have the details beyond that right now sorry but that passes the sniff test for me at least.
While I agree with the general sentiment, there's a bit of pedantry for you here: Typescript itself removes all the type information when you compile, it's not about the developer doing that themselves outside the context of the compiler. But that is a moot point with regard to the suitability of the features as the article itself suggests-- TS doesn't break your enums during compilation, a suitable JS representation is emitted.
Occasionally I think I might want to use enums in a place but then find that other solutions like const collection objects or plain constants works fine enough for the purpose at hand. I don't think it's a problem if folks use enums though, it's really not a big deal.
The reasoning behind most of this, that the output JS is not a strict subset of the input is silly and imho represent a misunderstanding of what Typescript is going for. It’s a full fledged language that compiles to readable JS, not just annotated JavaScript.
There are many features in Typescript where it simply isn’t just outputting a subset of the input, and many of them are the best parts of Typescript.
If you just want JavaScript with types, there are other languages that do that, but Typescript offers so much more.
I’m curious what they mean by “expression-level syntax” specifically, because they certainly added a lot of syntax that isn’t ECMA and aren’t just Babel, backporting future ECMA features.
As long as it's stage 3 EcmaScript, I (and the TS team, AFAIK) wouldn't count that as TS syntax - that's actual ES for all intents and purposes.
Other than that it's mostly syntax that affects how your code runs - i.e. if you were to remove all syntax that TS adds, the code should still run without further modifications in any interpreter that understands ES. (And features like enums don't satisfy this condition, hence why they don't match TS's design goals.)
That seems very readable, especially in comparison to something like Dart or Elm’s output, both of which can output thousands of lines from something as simple as your example.
I don’t really see the value proposition of having a compilation step that also prioritises readability when you have source maps.
I love Dart personally and I mostly see it’s compile to nonsense looking code as a feature not a bug because it’s an ACTUAL compilation step worked on by ex Chrome team members who understand V8 internals not just code splitting and running terser over it and calling it a day. Want to get the same compilation optimisations that Google uses to run all of their multi billion dollar ad business? Cool, that’s enabled by default out of the box. [1]
The part where Dart on the web falls over for me is that they have shitty support right now for modern web APIs. They are building against some ancient version of Chrome’s WebIDL files so you can totally forget about things like web components for example.
So in that sense it doesn’t feel like a sensible choice in 2022 for basic web development which is a shame because it’s otherwise probably the best developer experience I’ve ever seen.
[1] I say this somewhat theoretically, I don’t know that Dart is in anyway an obvious thing to point to in terms of web performance from what I had seen casually. I think their goal there you can write huge business critical applications with stupidly large code bases and still get good performance. But nobody’s experience after using Google Ads is to talk about how snappy it was.
> I don’t really see the value proposition of having a compilation step that also prioritises readability when you have source maps.
It's to increase adoption. Some people still remember migrating to coffeescript and away from it. It's in line with tsc accepting regular JS files, the degrees of strictness, things like that. Typescript is optimized to be adopted by the maximum number of people, which in turn increases its usefulness, the feedback they can get, their influence on JS. People are going to write bindings for popular libraries, even migrate them.
Some other people (like at my job) have some people use typescript, and others the generated code. It makes debugging and reasoning about code easier.
As for Dart, I'm not really convinced. The language seems to have the same philosophy as Go (incremental improvement over old technologies), and while for Go it works because Go is relatively "low level" (lower than Dart), for Dart it's just weird.
If you’re going to be working in a multi language code base I am with you. Handing people garbage looking generated or compiled code and saying work with this is going to require a solid set of well defined interfaces at a minimum and maybe like you said even giving up all of that and having to make one thing look like the other. All of what I said was under the assumption that you don’t need to think about JavaScript again.
Im making the argument that JavaScript is a target that we have all been collectively forced into due to the limitations of the web as a platform but short to medium term horizon that is changing where things like WASM are maturing and will let a lot of new options flourish (.NET folks seem to be probably leading this charge currently)
But just stopping to think about the implications of that kind of changing landscape and what’s coming, I don’t think aiming for 100% JS interop not just from a code perspective but it also the entire tooling and developer ecosystem perspective is going to be as important.
Again, I just think people are somewhat forced to at the moment because the web has always been a one language show. That wasn’t because JS was the best choice but rather a limitation of the platform itself which is already in the early stages of changing.
For Dart specifically I kind of get what you’re talking about I guess because it’s pretty commonly referred to as the best bits of JS and Java put together while ditching the worst parts of each so it’s clearly aimed at productivity for application sized code bases rather than something low level but again… that’s literally why they have a proper complication step because getting it down to something a lot more low level is exactly what a compiler is for. That doesn’t feel weird to me at all, that actually feels like an incredibly sensible choice.
Good points about JS being a target and that currently evolving. Lots of people these days code without any knowledge of assembly, maybe one day JS will be like that too. On the other hand, if your risk profile is more conservative (like many companies), you may want to let early adopters try out this "JS-less" world before investing in it. Since TS has a huge focus on large projects, and thus large companies, being conservative here makes sense.
For Dart, I wouldn't be so enthusiastic when describing it. The VM, hot reload, and all of that are impressive, but I'm not impressed by the language itself. The language seems to be an incremental improvement on 2005 Java and 2005 JavaScript, which are themselves not great. For example, it lacks data classes (records) and sealed classes. It took a long time to get nullability, the type checker (and system) are not impressive (things can easily fail at runtime).
Personally I don't get the argument against enums. From what I can tell it's purely to do with the symantics of what Typescript is, rather than any inherently bad property of enums.
211 comments
[ 0.16 ms ] story [ 277 ms ] threadHonestly most of these features are really useful, and as someone who never wants to work in raw JavaScript, I wish they were just added to JavaScript instead. Why shouldn’t JavaScript have enums, namespaces, private modifiers which aren’t #, and decorators? JS already gets new features which break backward-compatibility, mine as well add features which fix compatibility with TypeScript.
One cannot use Typescript without understanding Javascript, since every single Javascript library is not written in Typescript.
No one should need or worry about what an abstraction compiles to unless they’re specifically working on that abstraction or there’s a language bug.
> One should also understand the machine code that a traditional language compiles to and how the CPU executes it. Ultimately, your language compiles to machine code. /s
If libraries you use are all written in machine code, then sure, you should have an understanding of machine code. Your comparison clearly doesn't work here.
> No one should need or worry about what an abstraction compiles to unless they’re specifically working on that abstraction or there’s a language bug.
When an abstraction is that leaky, it's barely an abstraction. Typescript does force you to choose a Javascript version as a compilation target. Obviously you are forced to know what Javascript version supports what feature because Typescript isn't going to polyfill every missing feature depending on your Ecmascript target.
The problem with Typescript is that too many Typescript developers don't understand Javascript itself, which is an completely different issue. That and the obsession for some to reproduce JEE everywhere including in the browser...
I really dislike this tendency of certain Javascript developers to qualify combining OOP with Javascript as "not understanding the language".
Proxies and classes were definitely salvaged from ES4/ActionScript/Jscript.net.
We wouldn't be needing Typescript, had ES4 been adopted (ironically Microsoft was against it, because Silverlight...).
Someone definitely needs to write a book about the whole saga.
But before that there were countless competing models for creating objects or object factories.
Obviously, JavaScript is an object oriented language, too. You cannot escape that fact if you are determined to make the browser paint anything.
Classes effectively solved the "How?"
To pretend you don't need object orientation in JavaScript is really trying hard to make JavaScript into an entirely different language.
Java and C# had object factories even though in those languages classes could not be avoided. People wanted classes because they could not figure how to program without them.
> To pretend…
Don’t use this or new in your code and suddenly a tremendous amount of your code is exposed as unnecessary superfluous vanity. That isn’t making the language into something else.
Object factories were competing models (plural) for creating any object. A total replacement for classes and the like, not an augmentation.
Here's one such model:
And so you'd find this or any other model or multiple competing models in the very same code base.It sucked.
> Don’t use this or new in your code
You are going to be mutating the internal state of objects. Using this and new or not.
About the only positive that is qualitatively different is the simplicity of comparing the output to the input. But for someone building a large typescript codebase, and who uses sourcemaps, it's not really a big issue. There are many many languages that compile-to-JS, and I feel that insisting on 'purity' for purity's sake isn't really a good justification. That, TS as a super-set of JS instead of as a isomorphic mapping between constructs is a perfectly viable way to innovate in the language space.
But this doesn't justify removing every codegen feature. Namespaces and enums won't make your code less reasonable, and moreover they're just syntax sugar, they don't change actual JS semantics.
Besides, there is an straightforward way to remove enums from a program just like removing type annotations: Inline them as static fields of an object.
There is a simple syntactic transformation.
Namespaces can be translated in almost the exactly same way.It’d be really nice if they’d improve the ergonomics on this in some way (`type Values<T> = T[keyof T]` would reduce it to `type HttpMethod = Values<typeof HttpMethod>`, which is a start but not enough), to make it a genuine and suitable alternative to enum (minus the other frippery that’s generated) and const enum (because it’s pure JavaScript, not an extension).
Maybe recommend not to use Typescript altogether then. Their only reasoning for this seems to be that the features "be more likely to break when using build tools other than the official TypeScript compiler".
> What is there left to use in the end? Type annotations?
I've always used TS for the types and nothing more, and been happy with it. Seems to me that most of the JS community has come to adopt this approach over time, and I don't see what's bad about it.
That's what Typescript mostly is, that's where its success comes from. Type annotations for existing JS code. The vast majority of people need to type existing JS code, and compile to JS. A few minority need some specifics from the TS type system. Outside of that, there are other options.
But features like enums are part of the reason why we even have tools like TypeScript, because JavaScript lacks these features.
Switching between Python and TypeScript/JavaScript all the time, using ‘#’ to define private field feels weird and I personally prefer more explicit way of writing code. Plus AFAIK the ‘private someField’ is common in other languages (Java, Scala,…).
Explain the differences in the following to a new developer:
something - a regular variable
$something - just another regular variable with a fancy char
#something - now that’s a private variable
!something - this is… uhm… a regular variable cast to… eh… boolean, then inverted…
~something - type cast blah blah bit flipping magic
!!something - same as !something except inverted again, because using Boolean(something) is not 1337
//something - a comment
Yeah… personally, I much prefer the more explicit ways to write code. it’s cryptic enough as it is, why make it harder for your peers
I don’t particularly care what the generated JS looks like when I do all my work in TS, but I do care that the type system of the language I do work in works in a consistent manner.
I mostly agree with the rest of the article’s recommendations, but don’t fully agree with their reasoning.
- Yes, namespaces should be avoided, but not because they generate extra code. Namespaces are not recommended anymore and modules (actual JS modules not TS’ `module`) is the recommended approach now.
- Yep, private fields are better for ensuring a field is truly private.
- I think decorators are fine to use if you’re prepared for your code to potentially break in the future if they’re standardised. Developers use new/unstable features all the time (e.g. stage 0/1 JS proposals via Babel, nightly Rust toolchain). And I don’t believe the lack of standardisation of decorators should be a factor in deciding whether to use a library that requires these.
That’s nonsense, the code is there otherwise what are we talking about?
You just need to understand how some TS features map to JS, that’s all.
> The downside to enums comes from how they fit into the TypeScript language. TypeScript is supposed to be JavaScript, but with static type features added. If we remove all of the types from TypeScript code, what's left should be valid JavaScript code. The formal word used in the TypeScript documentation is "type-level extension": most TypeScript features are type-level extensions to JavaScript, and they don't affect the code's runtime behavior.
And:
> Most TypeScript features work in this way, following the type-level extension rule. To get JavaScript code, the compiler simply removes the type annotations.
> Unfortunately, enums break this rule.
And then there is an explanation about why this is important: it makes life hard for tooling, especially fast tooling; for in the absence of such features, JavaScript tooling can support TypeScript with little bother, just dropping the TypeScript bits and getting equivalent JavaScript; but in the presence of such features, they have to either use tsc (slow!) or implement more TypeScript-specific stuff.
Compilation of most TypeScript features to JavaScript simply removes the TypeScript bits. Enums, however, have to be transformed, adding to the output JavaScript something that was not in the source JavaScript.
[0]: https://github.com/evanw/esbuild/issues/128
[1]: https://github.com/microsoft/TypeScript (repo description)
When I initially saw Typescript, I thought the point was to add features from strongly-typed languages and then transpile into Javascript. (IE, a more modern version of GWT, a Java to Javascript transpiler.)
The point of the article, though, is that Typescript works best when its extensions to the language can simply be dropped. That's clearly a "we've worked with this for many years and this is a big lesson from experience" statement, so I wouldn't discount it.
It's the other tools that author is/was using that are having issues. It's silly to provide blanket statements about very useful features like enums or namespaces just because some third-party tool is struggling with them IMO.
Namespaces mKe no sense to me. It’s probably because Microsoft drives TypeScript, but even though I was a C# developer for 10 years before moving on, they’ve just always been terrible to me. Their functionality is the sort of thing that is nice in theory, but really terrible in real world projects that run for years with variously skilled developers in a hurry.
Private is silly to me, but this is mostly because classes are silly to me. I can see why you’d want it if you use a lot of classes, I just don’t see why you would do that unless you’re trying to code C# in TypeScript. One of the things I loved the most about switching from C# to Python was how easy it was to use dictionaries and how powerful they were. The combination of TypeScript interfaces, Types and maps is the same brilliance with type safety. But once again, it’s sort of the thing where classes sometimes make sense, and when they do, so might private.
- Avoid enum with negative integers because it generates code that cannot be optimized by the JS engine
- Prefer ESM modules to namespaces, because the first can be tree-shaked.
[0] https://news.ycombinator.com/item?id=30010017
Everything has its place, and for allot of the features of TypeScript I think they are designed to be useful when you have a large number of developers working on incredibly large codebases.
I suppose in some ways it's like C++, you can decide to fully embrace all of its features, or code in a much more C like way, just taking advantage of classes.
It comes down to personal/team preference, what works for you.
Personally with TypeScript I'm inclined to code in a "closer to JavaScript" way (but taking full advantage of types obviously), but would happily work in whatever style was prevailing in the project.
We use enums and private keywords. With the private keywords, all I care about is that it is logically correct. We use private when things are truly private, i.e. they are only called from within the same class and don't need to be made visible to the view template or outside of the class. I honestly don't care what this transpiles down to, the point for us at least is not to make things "truly private" (good luck with that in JavaScript). It's simply to compiler-enforce rules. We also have ESLint to ensure that our private fields are all below the public ones to keep things nice and neat.
I also enforce that we actually make things as public although that isn't needed, and I enforce returning void.
So instead of:
someMethod() {}
I have us use:
public someMethod(): void {}
Just to state what you intend.
I realize "I don't care what this transpiles down to" might really irk some people, but I really don't. In our C# back-end I am much more strict about this stuff, but in JS at the moment given the standardization of the #private fields and the fact I consider them really ugly, I honestly don't care. Just give me a clean code base that enforces we can't reference private fields and methods from our templates.
For enums, I recently wrote a method that does exactly what this article says not to do, use it for GET, POST, and PUT.
What would be a cleaner way to write this? If it has to be refactored into something much "uglier" I don't think I'd prefer it.
This is a service I wrote that handles real-time progress (i.e. show an accurate progress bar when downloading files). I think this is clean and logical.It’s not just JavaScript. With reflection in C# and Java, you can mess around with private variables from outside the classes. For Java, this can have some pretty interesting results, such as 2+2 being equal to 5.[0] The whole point of compiler level annotations is to keep good programmers honest.
If some devious JavaScript developer wants to ruin your library, that's their fault.
[0]: https://codegolf.stackexchange.com/a/28818/13944
Worth mentioning that you can disallow reflection via the security manager, at least in earlier versions of the JVM.
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.
Usually it's just
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).
If you just use the value within your code the runtime representation does not matter, you're completely right.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
https://www.typescriptlang.org/docs/handbook/2/everyday-type...
What value do you think it offers here?
> 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 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.
Am I missing something in how to use this?
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:
To be clear, the enum is also defined at runtime. So this specifically isn't a difference.
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.
Substantially. Look at the generated code for an enum.
Also, this approach does not suffer the problems described by the article.
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:
when, we would obviously write the following in modern JS: 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).
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.
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.
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.
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
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.
Do they?
It seems to me that namespaces are more powerful and convenient than JS modules as they enable more structure.
I have only dabbled in TS and am not sure how useful they are there. But I assume they would be similar to PHP/C#/Clojure namespaces on a conceptual level.
The private keyword is obviously preferable to "#" in environments in which you are targeting older ES versions.
When you expose TypeScript code to JavaScript consumers you absolutely must validate all incoming data anyway, whatever type you declare.
Generally speaking, a Javascript user has access to the same type script language features (including hints, auto-completion,...) as a typescript user.
Even the most primitive text editor supports language server protocol nowadays.
The library your interoperating with does not even have to be written in typescript if the author provided @typedef JSDocs.
const enums break this because no value of this type exists
Well designed Abstractions are good, not bad…
reviews Angular app
oh
What? This is IMO bad advice. Having a sum type is quite handy for general type-checking, at least insofar as the type truly is an enumerated type (i.e. all possible values are known at design-time). There have been times when TypeScript enums have been indispensable to me when declaring the external interface to some client-facing API. Whatever the API boundary is, a sum type is useful.
Also, TypeScript gives general intersection types, which is quite rare among its peers. (What I wouldn't give some days for mypy to have intersection types, or bivariant functions, or conditional types, or ...)
The only other impetus for this post I can imagine is some weird desire to see typescript as a strictly separate layer above JavaScript that "could be removed" if we wanted it to be. I suppose that was the project's original telos, but today the abstraction is leaky in a few places. I'm a world where JSX is common and radically departs from what would be considered normal JS, I don't see a problem with TypeScript being leaky here and there. Hell, I'd prefer TS to be leakier and add opt-in runtime checking (i.e. code gen), because it would make my life easier in certain instances.
That's a problem a lot of developers won't have. I've covered a lot of ground without ever finding a tool that either hasn't been retrofit to support TypeScript or that has issues that are tickled by TypeScript generated code. My blunt recommendation if somebody hits that problem is to find a better tool.
It's true that enums are harder to support for alternative compilers. Especially `const enum` seems to be harder.
As long as it is supported — and at least esbuild does, who cares (as a user). Should I start avoiding every JS feature that is hard to implement in alternative JS runtimes?
At least TypeScript cares enough to have added the `isolatedModules` and the `preserveValueImports` flags:
https://www.typescriptlang.org/tsconfig#isolatedModules
https://www.typescriptlang.org/tsconfig#preserveValueImports
Actually, as Amish communities are part of the human ecosystem, maybe we should also restrict anything we build in the real world to something that can be useful to the Amish and stop building anything requiring a power grid ?
You can't test if a given string is an element of a union without writing a custom helper and/or allocating a runtime array of the values.
On the other hand, if you don't need to test unknown strings, then union types disappear at compile time, whereas enums are compiled to real, runtime, objects.
Enums don't look like naked strings in the code. Frankly, it's just nicer on my brain to see `return Color.Red` than `return "red"` and wonder if "red" is just some random text or if it has semantic meaning. Hopefully your IDE is smart enough to take you to where "red" is defined as part of a union type when you want to see other options. Granted- the most popular editors ARE smart enough to do that, but that doesn't help when just reading code with my eyes instead of my hands, or in patches/diffs.
- https://stackoverflow.com/questions/40275832/typescript-has-...
- https://github.com/microsoft/TypeScript/issues/32690
In a TS codebase I'm currently working on, we have the policy of never using "plain" TS enums. Instead, we have a tool that generates our own enum objects using a schema and a generator script for the cases where we want "rich" enums. For other use cases, we use string unions a lot (in combination with a helper function that allows exhaustive matching).
They can also be used in the more traditional form to represent some arbitrary values.
They shouldn't be overused, though.
As for the fact that types cannot simply be stripped out, I've found building using plain tsc and have the bundler target tsc's output directory. This separation is needed since most tools don't support TypeScript project references anyway which I find extremely useful for organizing internal modules.
I most strongly disagree with the recommendation against enums. Realistically, you will probably never run into a compiler bug from enum emit; maybe something like this might happen with a very complicated runtime feature, but enum emit is dead-simple and hard to get wrong (at least if your toolchain has any tests at all, which it presumably should). And they're generally convenient and fill a useful niche, especially numeric enums with implicitly assigned values. (I'm also curious what the article's authors think of const enums.)
Namespaces have been soft-deprecated, modules are pretty much just better, and so I quite agree that you shouldn't use them, though I'm not sure the risk of compiler bugs is the most compelling argument against. (It is more compelling than with enums, since the required code transformations are much less trivial.)
Decorators, especially with metadata, facilitate lots of useful things that otherwise just aren't possible in TypeScript. It's also the case (though the authors seem unaware of this) that they will never be standardized in the current form that TypeScript has them, because they were based on an earlier version of the design that has since been pretty explicitly rejected. The risk isn't that decorators are never standardized; if that happens then TypeScript will just keep the current design forever and things will be mostly fine. The risk is that they get standardized in an incompatible form and then you have an interesting migration ahead of you. TC39 won't do this lightly, but no one knows exactly what the future holds. So it is a tradeoff to think carefully about, though in the end reasonable people will disagree.
# vs. private is mostly a matter of style/taste, with two exceptions. First, if you have any code on your page that you don't trust not to be doing weird things, strongly consider #, since it provides protection against corruption of internal state. Second, if you have to support older browsers that don't support #, then don't use it; the compiler can downlevel it, but only at significant code-size and performance cost that you don't want to pay (and debugging it in the browser will also be annoying).
Do the authors also disfavor parameter properties? Those also require emit beyond just stripping types, but are super-convenient and useful and don't really conceptually complicate things.
Incidentally, the feature at the top of my own list of "TypeScript features to avoid" (other than namespaces and other soft-deprecated features) is something entirely different: conditional types. Most other advanced type-system features behave reasonably predictably most of the time, but conditional types are very demanding of maintainers' knowledge of fiddly type-system details. I'm not saying it's never worth it (and in particular the built-in utility types are usually fine even though they're conditional under the hood), but whenever possible I try to reach for something else.
That's true, but diagnosing other bugs is an absolute pain in the butt when your enum value at runtime is 0, 1, or 2. You get all of the readability of C with none of the performance :)
Angular 2 would look very different without it.
Is there really such a thing? Everyone seems to be writing TS with the "fake it until you make it" mantra, never quite reaching the "make it" phase. People still use "interface" and "type" interchangeably without rhyme or reason. Or "import" vs "import type". No one knows what they are doing in TS. Or why. Just look at this entire comment section.
The real trouble occurs when TypeScript implements something because that looks like the way things are heading, but then they don’t head that way, and JavaScript and TypeScript diverge incompatibly. The type stuff is generally fairly safe, and fundamentally necessary, but some of the other stuff they’ve added isn’t safe and isn’t… as necessary, at least. Decorators are the current prime case of this divergence problem: they added a feature because it was useful, lots of people wanted it, and that was what people expected JavaScript to get before long, and some were using already through a Babel extension but people were sad about having to choose between nifty features (Babel) and types (TypeScript); and then because they’d added something not in JavaScript, why not go a bit further? and so reflection metadata came along; and then… oh, turns out decorators are actually heading in a completely different and extremely incompatible direction in TC39 now, but people are depending on our old way and PANIC! It’s been a whole lot of bother that will continue to cause even more trouble, especially when they try to switch over to the new, if it gets stabilised—that’s going to be an extremely painful disaster for many projects, because “experimental” or not, it’s a widely-used feature of TypeScript.
This is not the only such case; there’s one other that caused a lot of bother comparatively recently, but I can’t think what it was (I don’t work in TypeScript much).
Sciter used what was loosely a fork of JavaScript when it started, and diverged, for quite decent reasons in some cases I will admit, but this divergence caused more and more trouble, until recently they gave up and switched to JavaScript, … except with a couple of incompatible deviations already and more on the table as probabilities. Sigh. I had hoped a lesson had been learned.
So yeah, I’m content to call these things misfeatures. TypeScript overstepped its bounds for reasons that seemed good at the time, and may even have been important for social reasons at the time, but you’re better to avoid these features.
Otherwise I'm not aware of any cases besides decorators where TypeScript did something that was incompatible with a later ECMAScript development.
In it he talked about how Angular 2 pushed decorators in to TS. And to this day, Angular is the only major JS thing that I can think of that uses decorators.
Creating that ng-abomination was not enough. No. Google also had to poison a perfectly fine language.
https://github.com/emberjs/rfcs/blob/master/text/0408-decora...
https://guides.emberjs.com/release/in-depth-topics/native-cl...
Unless you're a solo developer, what's more important it to agree on languages and conventions that apply to your team's projects. Once that's done, any change to the team's work process should be a conscious, measured decision.
Note that I also used CoffeeScript 10ish years ago and still consider it to be a superior experience to working in plain-old Javascript.
I’m genuinely looking forward to the day where we have other viable options for the DOM. I see TypeScript at best as a temporary band aid because you’re still stuck in the god awful NPM ecosystem at the end of the day.
It won’t get me DOM level access but I can at least move a lot of my code there. I’m also quietly hopeful that canvas based rendering can make some huge improvements in the next few years so it doesn’t feel like Flash 2.0 but I’m ready to at least start thinking about letting go of the DOM as the thing I have to care about.
Until then I’m having a good time with Lit (lit.dev) for building web apps that need to be super snappy and “web feeling” which is still basically every customer facing thing.
But in a dream scenario I would way rather be writing apps in Flutter which was at least built from the ground up for building complex user interfaces in a sensible way, but that whole ecosystem is still in some very early days on the web and isn’t a good choice right now for most things, hoping that changes in a few years as they also seem to be targeting the WASM + Canvas path and the web as a platform isn’t there on that yet and neither are they.
Secondly, there is no way for automated agents to extract content from your user interface. This includes search engines, browser extensions, the browser itself, or your end users. I think that goes against what the web is, and I hope other devs agree. The mutability of HTML (and thus the DOM that represents it at runtime) is a strength, not a weakness
I apologise if I get some minor details wrong here as I am doing this on a phone and recalling this from memory because I don’t have the time to grab the sources right now.
However… the short version of the plan to solve this that I seem to recall to this:
The Flutter team specifically seemed to indicate that they are already used to operating in non DOM environments where they have to support accessibility across Android, iOS, MacOS, Linux and Windows that doing it on web actually isn’t that big a deal as it first seems.
They already have all of the code in place that builds a full tree (like the DOM) which does a complete mapping between every element (widgets in Flutter lingo) on the canvas to their respective bits of accessibility info. They then just take that tree and hook it up to the respective accessibility APIs that each platform exposes.
At no point have they indicated that this looked like it was going to be a serious roadblock or challenge for them. I believe them and they have a history of this approach working elsewhere.
There is just too much money riding on this investment for them not to get accessibility 100% correct in a web native way.
From there if you are able to expose everything through accessibility APIs then you presumably also have everything you need as a search engine or an adblocker to actively read and modify that canvas. At this point the entire argument that it’s just an opaque set of pixels seems to vanish for me.
This is also AFAIK already a solved problem for them in other products where they are using canvas based rendering such as Google Earth and Google Docs.
I don’t have the details beyond that right now sorry but that passes the sniff test for me at least.
That's the problem, there isnt an accessibility API for the web, is there?
EDIT: At least not one that works without DOM
Occasionally I think I might want to use enums in a place but then find that other solutions like const collection objects or plain constants works fine enough for the purpose at hand. I don't think it's a problem if folks use enums though, it's really not a big deal.
There are many features in Typescript where it simply isn’t just outputting a subset of the input, and many of them are the best parts of Typescript.
If you just want JavaScript with types, there are other languages that do that, but Typescript offers so much more.
It's pretty what TypeScript's going for though:
> Avoid adding expression-level syntax.
https://github.com/Microsoft/TypeScript/wiki/TypeScript-Desi...
Other than that it's mostly syntax that affects how your code runs - i.e. if you were to remove all syntax that TS adds, the code should still run without further modifications in any interpreter that understands ES. (And features like enums don't satisfy this condition, hence why they don't match TS's design goals.)
Compiling to readable JS is not one of TS's goals. For example:
https://www.typescriptlang.org/play?noImplicitAny=false&targ...
From the language goals
> 4. Emit clean, idiomatic, recognizable JavaScript code.
https://github.com/Microsoft/TypeScript/wiki/TypeScript-Desi...
I love Dart personally and I mostly see it’s compile to nonsense looking code as a feature not a bug because it’s an ACTUAL compilation step worked on by ex Chrome team members who understand V8 internals not just code splitting and running terser over it and calling it a day. Want to get the same compilation optimisations that Google uses to run all of their multi billion dollar ad business? Cool, that’s enabled by default out of the box. [1]
The part where Dart on the web falls over for me is that they have shitty support right now for modern web APIs. They are building against some ancient version of Chrome’s WebIDL files so you can totally forget about things like web components for example.
So in that sense it doesn’t feel like a sensible choice in 2022 for basic web development which is a shame because it’s otherwise probably the best developer experience I’ve ever seen.
[1] I say this somewhat theoretically, I don’t know that Dart is in anyway an obvious thing to point to in terms of web performance from what I had seen casually. I think their goal there you can write huge business critical applications with stupidly large code bases and still get good performance. But nobody’s experience after using Google Ads is to talk about how snappy it was.
It's to increase adoption. Some people still remember migrating to coffeescript and away from it. It's in line with tsc accepting regular JS files, the degrees of strictness, things like that. Typescript is optimized to be adopted by the maximum number of people, which in turn increases its usefulness, the feedback they can get, their influence on JS. People are going to write bindings for popular libraries, even migrate them.
Some other people (like at my job) have some people use typescript, and others the generated code. It makes debugging and reasoning about code easier.
As for Dart, I'm not really convinced. The language seems to have the same philosophy as Go (incremental improvement over old technologies), and while for Go it works because Go is relatively "low level" (lower than Dart), for Dart it's just weird.
Im making the argument that JavaScript is a target that we have all been collectively forced into due to the limitations of the web as a platform but short to medium term horizon that is changing where things like WASM are maturing and will let a lot of new options flourish (.NET folks seem to be probably leading this charge currently)
But just stopping to think about the implications of that kind of changing landscape and what’s coming, I don’t think aiming for 100% JS interop not just from a code perspective but it also the entire tooling and developer ecosystem perspective is going to be as important.
Again, I just think people are somewhat forced to at the moment because the web has always been a one language show. That wasn’t because JS was the best choice but rather a limitation of the platform itself which is already in the early stages of changing.
For Dart specifically I kind of get what you’re talking about I guess because it’s pretty commonly referred to as the best bits of JS and Java put together while ditching the worst parts of each so it’s clearly aimed at productivity for application sized code bases rather than something low level but again… that’s literally why they have a proper complication step because getting it down to something a lot more low level is exactly what a compiler is for. That doesn’t feel weird to me at all, that actually feels like an incredibly sensible choice.
For Dart, I wouldn't be so enthusiastic when describing it. The VM, hot reload, and all of that are impressive, but I'm not impressed by the language itself. The language seems to be an incremental improvement on 2005 Java and 2005 JavaScript, which are themselves not great. For example, it lacks data classes (records) and sealed classes. It took a long time to get nullability, the type checker (and system) are not impressive (things can easily fail at runtime).
There are features of the type system you should definitely avoid unless you're writing a library. Enums aren't it.