Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
TypeScript’s quirks: How inconsistencies make the language more complex (asana.com)
312 points by lobster_roll on Jan 31, 2020 | hide | past | favorite | 209 comments


These can make sense if you think of TS as better-checked JavaScript, not as a from-scratch design for a statically typed language. Excess property checks catch a common JavaScript bug (typoed property names), structural static typing matches the dynamic duck typing JavaScript authors had already been working with (though I see how nominal would be useful), and it's awesome they found a workable hack for discriminated unions where the code and objects look like what you'd do in vanilla JS, but checked (and requiring the class to have a specific structure seems fine as limitations go).

I get how folks coming from "natively" static type systems could find all this weird, but I think by targeting "JS but with better checks" instead of a more fundamental redesign, the TypeScript crew have managed to build something that can get traction where other efforts have not.


As someone that uses the language everyday, these are real quirks on the language, but they are not showstoppers. The benefits of using exactly the same language in front-end and back-end, with shared types and everything, largely offset the small quirks.

However sometimes I kinda miss some functional stuff that you can for example find in Scala or Rust, like pattern matching. Specifically, we've had experience using/developing an input validation library for the backend in Typescript (https://github.com/StrontiumJS/Framework/) that is composable and also returns the correct type in one go. We used a lot of try catches for that, but that makes validation slow. So we started using "maybe promises" which are similar to Scala's Futures with Success/Failure. But the fact you don't have pattern matching in Typescript like in Scala makes it a bit ugly.


Have you considered OCaml with BuckleScript or js_of_ocaml? The language that inspired Rust's nods to FP can be compiled to mostly-idiomatic Javascript.


Pattern matching seems to be a current proposal, also driven by Microsoft: https://github.com/tc39/proposal-pattern-matching


Pattern matching is definitely something I also miss. For input validation and Optional/Either types, io-ts/fp-ts (https://github.com/gcanti/io-ts) fulfill my needs perfectly, especially io-ts is something I can't recommend enough. Saved me so many headaches from sudden breaking changes in APIs we consume.


You should try using computed propeties, it's the closest/cleanest way to pattern matching until tc39 will pass the pending proposal.


Yup. Rather than bringing 100,000 devs from one typed language to another, Typescript brings 1,000,000 devs from untyped to typed.


There's no such thing as an untyped programming language that's used in the real world. JavaScript and typescript definitely aren't. Maybe you mean dynamically typed


This is just a question of semantics. In mainstream programming language jargon it’s ”dynamically typed”, while in (applied) type theory it’s ”untyped” or, perhaps more accurately ”monotyped” or ”unityped”, because types are fundamentally invariants of a program that can be verified without executing it.


There’s always that “intellectual” who would say something smart while being absolutely wrong.


Structural typing has the benefit of making it easier to code to an interface and reminds me of Go. If you have a function that requires a method off a class (say, `poop`), then rather than declaring it to accept `Cat | Dog` (or overload it, in e.g. Java), you code it to accept an object with a single function (`pooper: { poop: VoidFunction}`). Then anything that implements it can use it, testing is typed and easy, and changing the class is less likely to affect the consuming method.

Its such a powerful pattern, I'm a bit surprised to see so much negativity about it. I'm aware there's alternatives and reasoned opinions, but I bet a significant number of people have never seen or programmed in this style and aren't aware of what they are missing. For me, I can't imagine using another typed language that doesn't support implicit interfaces in this manner.


If you can pass a Cat to a Dog function, where is the better checking?

That would be one of the hello-world test cases for a type checker bolted on to a dynamic language.

That type checker itself has introduced the syntax for declaring Cat and Dog classes; yet is neglecting to do the obvious with it.


Note that you can only pass Cat to a Dog function if:

* All fields of Dog are also present in Cat, with compatible type signatures

* All functions of Dog are also present in Cat, with comaptible type signatures

"compatible type signatures" does seem to leave some suprising co/contravariance holes still when using wider union types, but C#/Java arrays also have co/contravariance holes and we don't automatically write off their entire type systems because of it. Or, well, at least I don't ;).

As a meaningless point of ancedata: Making these types sufficiently equivalent to fall through this type safety hole appears to be rare enough that I've never done so by accident - allowing me to be suprised by the article's example of passing a class Cat to a class Dog-accepting function being legal. While typescript has never been my primary dayjob, it's been a significant secondary part of my dayjob and hobby junk.

(EDIT: Minor clarifications)



C# has a great type system, but everything has ToString() and sometimes that doesn't output something sensible, and gets implicitly converted, leading to garbage.

Still much better than working in C or some other weakly typed languages.


As a long time TypeScript user, I do think this is a design mistake with the language. For example, it breaks instanceof checks.

However:

- The language primarily uses structural typing. If two classes are compatible, it's not completely unreasonable to expect them to also behave in a structural way (though I would prefer them not to).

- Adding a private field to a class makes it work as a nominal type (https://michalzalecki.com/nominal-typing-in-typescript/#appr...). It feels a bit hacky and it's not very well documented, but you could argue that until a class has some private state you can't publicly access, there's no reason to prevent you from declaring a compatible class.

- From my experience idiomatic TypeScript rarely uses classes, the primary exception being React components. In teams I've been a part of we've always written TypeScript (and JavaScript) like an (impure) functional language, taking full advantage of ADTs, simple structurally typed data and more-or-less pure functions.


taking full advantage of ADTs What? There is no clean way to switch on the types of an ADT on typescript, I believe.


Well discriminated unions are TypeScript's implementation of ADTs (but whether they are "clean" is could be debated)


The problem is less that the syntax exists and more that people are used to nominal typing so get confused with edge cases where structural typing departs from it. The type checker isn’t breaking any promises it made it’s just slightly different from people’s mental model of how types should work from experience of other languages.

It’s just a different kettle of fish to learn.


CatDog typing sure looks, walks, and quacks a lot like Duck typing.

https://en.wikipedia.org/wiki/CatDog

https://en.wikipedia.org/wiki/Duck_typing


From your second link:

https://en.wikipedia.org/wiki/Duck_typing#Structural_type_sy...

> Duck typing is similar to, but distinct from structural typing. Structural typing is a static typing system that determines type compatibility and equivalence by a type's structure, whereas duck typing is dynamic and determines type compatibility by only that part of a type's structure that is accessed during run time.


That looks like ooks like "original research". It's obvious that what is described as dynamic-only duck typing could be statically checked. So that is to say, a given expression foo.bar in the program could be statically checked to make sure that all possible values that foo takes on have property bar. If three classes A, B, C in the program have property foo, then this means verifying that foo is A | B | C. Thus, "static duck typing".


Kind of but not really. If you ask a "duck" to quack, duck typing is satisfied with any object that implements the quack method.

With structural typing, the type system will check that the "duck" not only quacks but also looks like and walks like a duck, before it is allowed to be asked to quack.

EDIT: of course with small enough interfaces you can get structural typing pretty close to duck typing, if you have interfaces "QuacksLikeDuck", "WalksLikeDuck", "LooksLikeDuck", etc. instead of one big "Duck" interface.


> If you can pass a Cat to a Dog function, where is the better checking?

If you have compatible method, fields and types, in a duck-typed ecosystem like Javascript it is a major boon that it is possible to interchange them, because it happens all the time in the ecosystem.

The standard way to do enforce exactly the right interface in typescript if you need to is to add a `type: "cat" | "dog"` field to your Animal interface and have the Cat interface have `type: "cat"`. If works well and is very similar to a JVM Class object or .NET Type object.

Structural typing, literal types and dependant types are major blessings to have available sometimes, and I'd really like languages to adopt some of Typescripts power and simplicity in this. So far I've only seen Julia have some of it, although Scala 3 also introduces some bits.


What is a boon is that I can write code that accesses obj.breed and have it work with any object that has a breed property.

That has some downsides; it can be abused to create a ball of mud.

However, a middle-of-the-road type check which insists that an object must have all of the properties of Dog, even ones you're not using, isn't very useful. It kills the above dynamism, and yet allows abuses to sneak through.

You will not find that cats are being passed to Dog functions until you try to maintain Dog. And then you will discover that, oops, everything you add to Dog has to be replicated in Cat.

Basically, a typecheck which says that an object coming in has to have all the properties of Dog might as well just be a subclass check. The smart way to ensure that a non-Dog class has all the properties of Dog is inheritance.

A properly done static version of this structural type check would validate that the object being passed to the function doesn't have all the properties of a Dog, but that it has all the properties which just that function requires (including transitively: through any functions it calls).


Whatever words you use, it simply conflicts with basic JS and is a serious flaw.

  const cat = new Cat();
  if (cat instanceof Dog) console.log('this should never be possible!');

What is this strange protective behavior towards TS, can we stick to logic please?


instanceof checks the prototype chain, which is a different concept from type. Prototype chains are dynamic and exist at runtime, while Typescript types are a static concept.


Nominal types for classes feels like a better decision in flow, no need to mangle with phantom private props which is not the place you want to make this decision (when you call there could be this decision made but I can’t think for use case, making this decision at definition is too rigid and makes less sense).


I still don't see the value added by TypeScript. My JavaScript tests catch typoed property names every time. I can't remember the last time I merged code with a typo in a property name. IMO, if you typoed a property name and your tests didn't catch it, it means that either your tests are poorly written or your object didn't need that property to begin with.


Some Typescript value added:

- Living documentation: types are a clear description of how your code and its data are laid out, and this structure is kept always up-to-date with the code, regardless of future changes. If the "documentation" (the typing) is wrong, the compiler will complain.

- Better tooling: autocomplete, more robust auto-refactoring

- Elimination of an entire class of bugs. This class may include: 1. `Cannot read property 'foo' of undefined` 2. forgeting a case in a switch statement 3. `'foo' is not a function` 4. in general, your code receiving data that isn't laid out in a way you expected

(Property typos is one within this class of bugs, but it is not all of it)

Yes tests catch these bugs as well. But tests are also manually written. And often not written at all. A compiler makes these (mundane) sanity checks automatic and mandatory.

---

Typescript's slogan is "JavaScript that scales" because static typing improves: dev documentation (via types living inside the code) and dev tooling (refactoring and autocomplete), both of which help immensely as a project grows in age (devs become forgetful), team size (new developers get lost), and convolutedness (e.g. tech debt).


Lets be honest here, these reasons are really weak. Living documentation - TypeScript is less readable. There is more stuff you have to read. Stuff that isn't even relevant. I see a "cars" variable. Oh no, I don't know what type it is, I am going to have a panic attack. Don't worry that cars.map((c) => c.model); is the next line or whatever. What type is it, I must know!

auto-refactoring.. yeah we all trust that. Until you come to the boundaries where all the types don't exist, like from the client to the server, to the database via its message system, etc. All the stuff coming in and out of your code is untyped.

Autocomplete works without typescript, even if it uses "the typescript engine". The types aren't needed.

Eliminates an entire class of bugs that nobody has ever had a problem with.


I get the impression you either don’t have much experience with Typescript or haven’t fully grokked it’s workflow, and have prematurely made up your mind. Your autocomplete comment for instance shows you aren’t aware of the extent to which Typescript vastly improves autocomplete.

Regarding typing at the boundaries, this is precisely where typescript shines. The types exist whether they are documented explicitly in your code (in the form of interfaces, types, protocols, etc), or not. This is where the documentation aspect come in to play - it is far easier to read a type definition than having to chase up API documentation, mentally parse tests, or console logging out api responses, to understand your boundary. Your refactoring argument is a strawman, because you wouldn’t just go and refactor names of things at the boundaries of your code in typescript, or any other language, without understanding the API spec. What typescript gives you is precisely the ability to refactor at the boundary when the spec changes and be much more confident that your changes aren’t going to break a whole bunch of thing, especially when it comes to the “class of bugs” that literally every JavaScript developer has dealt with, whether you are aware of them or not.


> All the stuff coming in and out of your code is untyped.

Until you automate building TypeScript definitions from your external code. We do this to varying degrees and are always trying to improve.


> TypeScript is less readable.

What makes you say so? I find it more readable than javascript.


Type systems can be thought of as low-cost (in terms of dev time) declarative tests. Which should leave you with less to test in your actual tests. They also give much quicker feedback than tests.


Is TypeScript low-cost? Most TS projects I've worked on had at least a 10 to 30 seconds build waiting time per iteration. This means that every time I make a change and run the tests, I need to wait 10 to 30 seconds.

With my iterative test-driven approach, I probably iterate about once every minute on average (also, my development iteration time gets faster as I become more experienced in the project). So 30 seconds represents 50% of my total development time. Meaning that I could have written 50% more tests or 50% more features in the same amount of time. And that's not even accounting for my loss of focus incurred as a result of waiting 30 seconds to see the result of my code change. By the time I see the result, the concept in my mind is not as fresh as it was 30 seconds earlier.


Are you aware of tsc options like --watch and --incremental ?

Combined with other live-reloading tools like nodemon or jest --watch you can have almost the same speed of your development-test-run-debug loops as in plain JS. But you do gain an additional instant feedback channel from the compiler.


No longer enjoying programming is a cost nobody can afford to pay. When I read the long list of c#, I mean typescript documentation, it gives me PTSD from c# and java, which people used to hate here, but now suddenly like and let me tell you it has nothing to do with microsoft social engineering.


Devs should enjoy writing trivial tests? Heaven forbid businesses might care how quickly something is delivered?

Let us know when you are willing join the rest of us in the real world.


I also used to think TS is like C#. Huge mistake. Try to approach it from a functional angle. The TS compiler code does not use the class keyword.


> My JavaScript tests catch typoed property names every time.

My typescript compiler catches typoed property names every time.

> I can't remember the last time I merged code with a typo in a property name.

I can't remember the last time my newly written code made it into the runtime/browser with a typo in a property name.

> ...if you typoed a property name and your tests didn't catch it, it means that either your tests are poorly written or your object didn't need that property to begin with.

The types are much more than just property names.


If TypeScript is used against bog-standard JS code, I can definitely appreciate your point.

For me, TypeScript really started to shine once I started taking advantage its generic support, higher-level utility types like Pick<> and Optional<> (and really anything involving keyof), and creating static type checks via the unknown and never types.

This is especially effective when working on legacy JS code bases where unit tests and other good engineering principles were not used well, or at all.


Do your JavaScript tests inform your IDE how to do code highlighting and completion and refactoring?


My JavaScript integration and unit tests do something better; they tell me exactly what features I broke as a result of my refactoring and let me fix things in the best way possible without regard for the previous structure of the code (which may no longer be relevant anyway; there is usually a good reason why we do refactorings in the first place! It's rarely just about renaming things).

I have many stories on the subject but most recently I did a significant refactoring for a JavaScript project (a popular open source library I created) to move from callbacks to async/await and was able to re-use pretty much all of the existing integration test logic with only aesthetic changes (to account for the new async/await API).

When I finished the refactoring, I had modified at least 70% of the entire source code of the project and I took it as an opportunity to make significant structural changes to my code to align with the new async/await flow in the most optimal way possible. This kind of big refactoring would have made code highlighting completely redundant and would have prevented me from looking at the problem with a fresh mind and taking into account the new features offered by async/await. It was a very successful refactoring; no new issues were uncovered after the point that I managed to get all the original tests passing again and the code was a lot simpler than before.

I think TS has a way of locking down old structures in a way that make it sub-optimal in the long run. Feature-oriented tests are by far the best way to ensure that product quality is maintained after a refactoring IMO.


I have the exact opposite opinion on "code lockdown". I believe that unit and integration tests heavily lock down code, so much as being mere checksums.

In a project with >90% coverage, I would find refactors dreadful. After any substantial change beyond adding stuff, there were always a myriad of tests failing. Not hard to fix, but simply tedious and dreadful.

On the other hand, I have an Elm project with no unit test, only some e2e tests. I can do huge transversal changes, and I don't find ot tedious at all. It gave me back the fun in programming. The Elm project is a game, so there should me an order of magnitude more bugs, but the opposite was the case.


Well designed tests lock down features not code. That's why I mostly write integration tests.

I write unit tests once the project is stable and usually only for parts of the logic that are either very complicated or operationally critical. I try to avoid implementing 'very complicated' modules as much as possible.


Do you mean e2e tests? Because those are the only ones that test features. Integration tests are the biggest offenders of "code lockdown". They don't really test features, yet they have to observe too many implementation details.

Unit tests can't really lock down code, or then we shouldn't call those "unit" anymore.


> TypeScript crew have managed to build something that can get traction where other efforts have not.

You mean Microsoft managed to get traction? It seems most TS proponents don't realize TS is a success because they fell for smart marketing and serious money behind the project, not because it's a better language than 'other efforts' like for example Purescript or Dart.

But every comment I make against TS seems futile. TS proponents defend their little language very fiercely regardless of all the flaws it has. It's almost worth a study how Microsoft managed to do that. Overall a real pity, because there are so many better options and ideas for those who want static typing. TS is actually the worse choice IMAO.


>It's almost worth a study how Microsoft managed to do that.

It's almost as if Microsoft learned the techniques of linguistic evangelism from Sun when they studied the ideas behind Java to design C#, but they left out the anti-linguistic-miscegenation Java-supremacist ideals of "100% Pure Java [TM]" when they made it easy to integrate other mongrel languages with COM and P/Invoke.

https://news.ycombinator.com/item?id=19571635


They did it by solving a great pain of a very large amount of people. Not many of these people would have been able to use Dart or PureScript.


Typescript unlike Dart, Purescript, Fable and BuckleScript is a superset of JavaScript so you don’t have to change anything in your existing JavaScript project to start using Typescript. Also JavaScript interop is a breeze and a huge percentage of the libraries have already typescript binding and it’s a huge advantage compared to using the aforementioned languages.


The following is purely anecdotally, my opinions is solely based on my personal experience with the language.

As someone who learned TypeScript just by using it and without any serious "study", I'll have to disagree the basic premise of the article.

I already knew about almost everything the article mentioned and nothing really seemed weird to me at the time.

The only point that I did not know about is type narrowing not working on nested objects, because that came up exactly 0 times in about 50k lines of TypeScript so far.

Half of the points made are actually TypeScript being lenient (interfaces allowing excess properties, classes working like interfaces). In my book this makes the TS easier to work with, because it gets out of the way when it's not needed.

Classes essentially working like interfaces is probably a result of TS wanting to be backward compatible with JS, because when you enrich your existing JS lib with type information, TS can't know about your "classes" - they're a runtime thing. Treating them the same as interfaces can make this much nicer.

It also allows 3rd party code to add extra properties to your new-instanciated object and using an interface to reflect the change, but still being able to pass it to stuff expecting the original thing. This way you can have type safety without losing JS features.


> TS can't know about your "classes" - they're a runtime thing. Treating them the same as interfaces can make this much nicer.

No–this is a very surprising and unsound choice and not at all what you would expect if you previously saw that TypeScript lifts classes into types.

> This way you can have type safety without losing JS features.

IMHO, you can't get type safety out of an unsound type system. (Which TypeScript is, by choice.) I think surprises like the above drive that point home.


TypeScript’s goal isn’t to be sound. Its goal is to be a balance between type-safety and effort. Full type safety would require a lot of runtime checks injected into the final JavaScript, which TypeScript intentionally avoids.

JavaScript will never be fully type-safe without fundamental changes, which will probably never happen.


I understand that, which is why I said 'Which TypeScript is [unsound], by choice'.

Disagree that full type safety would require a lot of runtime checks. That's not the experience I've had in ReasonML.


ReasonML is exactly the kind of backwards incompatible change that would be necessary. You cannot mix reasonML and JavaScript freely together.


"Full type safety would require a lot of runtime checks injected into the final JavaScript"

Why would this be the case?


Array access is a good example.

Many type-safe languages throw an exception if you attempt to access an element in an array that is out of bounds but javascript just returns undefined.

Typescript ignores the fact that accessing an arbitrary element of an array could return undefined. If it did not ignore this fact then it would force you to deal with the potential undefined value retrieved from any array access (i.e you'd need to write a runtime check)


Index unsafety w.r.t. bounds and `undefined` is something TS would like to fix; but it's not ergonomic to fix until there are, something like automatic refinement range types applied to array length fields. Without it, TS'd get in the way, demanding `undefined` checks it almost definitely didn't need. Those range types and their complexity are, ultimately, what currently block TS from making these indexed accesses safer.


Some languages do force you to check any time you access an element. If the rest of the language is in line with this, then it's actually amazing. Option/Maybe types help here, pattern matching lists also let's you make sure you handle all cases.

In some instances it makes sense to use a special NonEmptyList type that guarantees there is always at least one element, so you don't need to check.


[flagged]


There's a thing called dynamic type checking. It's literally all type checking done at runtime. Even for mostly static typed languages you may want to lookup what a downcast is, it's not obscure.


I'm not talking about type checking with your run time code. Obviously this is exists. My mistake with the wording.

I am referring to the topic the poster is talking about, that is, type checking with type script and the precompile type checker of almost all statically typed languages.

The precompile type checks do not need run time code. These are separate things executed at separate times AND really you can get by with 100% static.


The thing is... it doesn't seem to matter. After 30-40k lines of TS code (coming from OCaml) I am still to find a bug where OCaml would have done a better job, except for missing features like nominally typed primitives and no pattern matching. And these are hopefully coming to TS/JS


I recommend looking at your frontend console logs for instances of type errors a la https://rollbar.com/blog/top-10-javascript-errors/


My console is fine, definitely do not experience these errors :)


> TS can't know about your "classes" - they're a runtime thing. Treating them the same as interfaces can make this much nicer.

It helps sticking mostly to interfaces as it helps managing expectations. Class information gets discarded during compilation and that brings with it some quirks. Java Generics is another example where type erasure leads to quirky surprises.


TypeScript’s goal isn’t absolute type safety, the goal is feedback (eg code completion) and earlier detection of many errors.


Yes, I realize that's not the goal which is why I said 'by choice'. I just think it's not the right approach ultimately. Other safer languages also reach the goals of code completion and error detection.


Most of the highly typed languages actually do a poor job with code completion and error handling, they were specifically designed with safety in mind and other aspects of the type system are given only secondary attention. TyoeScript is nice because it focuses on where type systems are really useful (feedback) as a first concern.


Fortunately, the typed AltJS language with the highest interest levels after TypeScript actually has really good code completion and error reporting :-)

Oh, and build speeds so fast that it's blink-and-you-miss-it. I think that's a pretty important part of useful feedback (and one that I hear TypeScript is not that great at).


Which language is that? Reason? How is its code completion experience anyways?

TypeScript has a dog of a compiler, I'll give you that. But that wasn't what I was referring to.



This article makes me think about the "rust compile times are terrible" blog post from the other day.

If you spend a lot of time with a technology, day in and day out, you get intimately familiar with all of its shortcomings, and sometimes you maybe lose some perspective.

Having used Rust (enough to see its innovations and promise) and TypeScript (daily), I think they are incredible languages and platforms. Sure, they have shortcomings, but I think it's important to keep in perspective the huge advances in the state of the art they have brought us for these trade offs.


Nothing wrong with highlighting areas for improvement, though.


I don’t agree with the article’s claim that discriminated unions are just a JS compat feature. Many APIs type JSON serialised entities using a string property and this feature makes it very easy to write interfaces and code to work with them.


also bear in mind that support for discriminated unions is what made typescript work nicely with redux actions - that is be able to strongly type actions/reducers/dispatch functions.

One thing I love about typescript is they care more for the actual JS/library ecosystem as-is, and don't design a language for an ecosystem that should-be.


FWIW, I'm a Redux maintainer, and I personally have never understood the point of trying to limit what actions are being dispatched. As long as your reducers and action creators are well typed, it shouldn't matter what other actions might be sent through.

On that note, our new Redux Toolkit package is written in TS, and designed to work great in TS apps with a minimal amount of type declarations needed. Really just declare the type of the action in the reducer, and get everything else for free. The new React-Redux hooks API is also a lot easier to use with TS as well.

I showed how to use both of those in the Redux Toolkit "Advanced Tutorial" docs page:

https://redux-toolkit.js.org/tutorials/advanced-tutorial

On a related note, I also recently put up a long blog post detailing my own journey learning and using TS, as both a lib maintainer and an app developer, with my takeaways on the pros and cons of using TS:

https://blog.isquaredsoftware.com/2019/11/blogged-answers-le...


> I personally have never understood the point of trying to limit what actions are being dispatched. As long as your reducers and action creators are well typed, it shouldn't matter what other actions might be sent through.

The value comes when you consider what happens if a reducer (or saga or whatever) is updated in a breaking way (or removed wholesale).

Example: consider a scenario where you use `connected-react-router`, your codebase fills up with history actions being dispatched, then one day you remove `connected-react-router`, or its major version is updated and introduces some breaking change.

If you have a union type that includes all your own actions plus the `LocationChangeAction` from connected-react-router (and you use is consistently - e.g. your components take `Dispatch<YourAction>`) then you'll learn about the breaking change when your app fails to compile.

The alternative is you catch it later than compile-time, at worst _much_ later.

You might decide that the overhead in this scenario isn't worth it, and that's your call to make (I believe it is worth it) but hopefully this gives an idea of the point.


Personally I always found it annoying to manually add a common tag to every interface/type on TS just to have it work like an union type. But since TS doesn't leak types to runtime that's the only way.


Author here! Yeah I think you are right that discriminated unions are useful more broadly than just legacy JS code.

That being said, I still think TypeScript's solution to handling them of using "type guards" where the type of a variable changes in different scopes is definitely designed to match common JS patterns at the cost of added complexity. Most other languages I know of only have a single type for a variable (and if you want to pattern match you must give each case a new variable name).


That’s control-flow sensitive type deduction though - not specific to union types. I agree that proper patten matching is much nicer - but unavailable in JS.


> Most other languages I know of only have a single type for a variable

This may be pedantic, but with subtyping many OO languages can give many different distinct types for a variable. In Java you can assign any non-primitive typed expression to a variable of type Object. So almost any expression in Java can be typed as Object.

What you are describing as novel is rather the phenomenon of type refinement in a pattern match or conditional expression. When an expression undergoes pattern matching, its type becomes increasingly refined. This is a useful feature in intermediate-to-advanced Haskell (known as GADT) as well as dependently typed languages.


> In Java you can assign any non-primitive typed expression to a variable of type Object.

Sure, but if you do this in Java you must use different variable names for the reference of type Animal and the reference of type Object.

I think algebraic data types and type refinement are great, but I’m not a fan of automagically changing the types of variables in different scopes if it can’t be applied consistently.


Hacklang also has something like type guards called type refinement [0].

I'm still kinda new to the idea of using conditional branches to inform static type checkers but it sounds like it's an idea that has been thought about in some depth [1][2][3] (i.e. doesn't sound like it was jimmy-rigged to match common JS patterns).

I personally love type refinement. Hacklang's type refinement was the first time it clicked that statically typed languages could _actually help_ you write code instead of get in the way.

[0] https://docs.hhvm.com/hack/types/type-refinement

[1] https://sites.cs.ucsb.edu/~benh/research/papers/kashyap13typ...

[2] Type refinements (page 23) https://drops.dagstuhl.de/opus/volltexte/2018/9219/pdf/LIPIc...

[3] Occurrence Typing (section 8.5) http://soft.vub.ac.be/Publications/2019/vub-soft-phd-19-02.p...


> Most other languages I know of only have a single type for a variable (and if you want to pattern match you must give each case a new variable name).

FWIW, Go also changes the variable's type in type-switch cases: https://tour.golang.org/methods/16


To add to what others have said: Kotlin is a language that does path sensitive (re-)typing of variables. This is often very convenient in the presence of OO-style polymorphism or in cases where the language supports ad-hoc unions.

Personally it doesn't feel complicated to use/understand (at least the kind of thing that these languages do)


Yeah, also this is basically how you'd implement tagged union types if you couldn't have actual algebraic data types like Haskell or ML.


This. My network code uses them everywhere on the client side. It's frankly amazing.

Your server side code should have runtime validation though. You can't and shouldn't rely on TypeScript there for security reasons.


I don't quite follow. Discriminated unions allow you to derive the type from the runtime validation. If the runtime finds these properties then it must be one of these, but if it finds those then it's one of those. If it can't fit into one of those type buckets as defined by runtime validation, then its <unknown>. The types flow from the validation algebraically.


I'm assuming you don't understand why you shouldn't use discriminated unions for server-side validation?

Client (JSON) messages will pretty much always start out as any, and you can't assume they fit your union any way shape or form. Taking the Cat|Dog example from the article, a client may pass {"kind":"cat", "bark": "woof"} and if you just go blindly from there to the union and then try to narrow that down, you've got yourself a problem.

You should use a library like JOI (or type guards with custom validation) to first make sure it fits your union type, then you can go wild with discriminated unions.

I always wanted a library that could perform runtime validation given a typescript type, but so far typescript doesn't expose enough type information in decorators for this.


But wouldn't what you're talking about just be a union type? We might be saying the same thing. The "discriminated" part of discriminated union means that you will be performing the runtime validation to check which of Cat or Dog it is, and only once it receives those validations do you have a derived type. By asserting that the object received is Dog only if it has kind:dog bark:string, you've narrowed your type at compile time to only types that have bark.


> But wouldn't what you're talking about just be a union type?

Not really, unless you define any as union of every possible type. Also TypeScript won't realize you have narrowed the type (because it doesn't use such a definition) with all your checking unless you cheat with type guards (which are really just a prettier cast with optional user-created validation).

Then there's also the fact that thanks to getters your narrowing from any may be incorrect, which is probably why you can only narrow to basic types or with an instanceof operator, neither of which will help you with your JSON input.

tl;dr: Discriminated unions won't help you when you're starting out from any, because any is not an union.


True, discriminated unions don't flow from any, but they can be useful for composing types from any.

I suppose I'm saying that the validation function itself is the precursor to discriminated unions being useful. <any> run through a type predicate function or an assertion type function (https://www.typescriptlang.org/docs/handbook/release-notes/t...) produces a typed object that conforms to your requirements for the type.

I definitely do wish there were some way to create these assertions/validators from the types automatically, but I think that might be impossible.

Apologies for any formatting issues, on mobile:

``` foo:any; isAnimal(foo); if(isDog(foo)){ // Dog } else { // Cat } }

function isAnimal(thing: any): asserts thing is Animal { if (thing.kind !== "dog" && thing.kind !== "cat && etc){ throw new AssertionError("Not an animal!"); } ```

// ...etc, except that string literals probably exist in an Array or similar, ie types that can be progressively derived / enhanced from the runtime code.


Typescript is great but it has its evils because it’s a superset of JavaScript. I really hope we get a modern language like c# where the types are guaranteed compile + runtime and you can’t any-ignore problems.

The more I use typescript I realize I want a more sound and stricter language. Like no prototype overriding at runtime.


> a modern language like c# where the types are guaranteed compile + runtime and you can’t any-ignore problem.

Come on, Typescript is more modern than C#. It's just started to being bloated a little bit since there are advanced type operators against structural typings. Or if you have to use a modern example, languages like Scala or Rust are more like it.

Also guaranteed compile + runtime is not what C# is. Probably Haskell is more closer.


Typescript is certainly newer than C#. So are Scala and Rust. Modernity and novelty are two different things, though.


Scala was released within a couple years of c#. Typescript and Rust were a decade later.


I actually like the unsoundness when I thought I would hate it. It rarely turns out to be a problem for me, and allows some designs I’d never be able to get away with in C#. What I really want from typescript are operators and extension methods (monkey patching is a bad hack), but those would break JavaScript supersetting (typescript isn’t allowed to desugar).


Noooo! Go away with those ideas! JavaScript is great because it’s not strict. That’s the flexibility that allows for speed.

I have like 30 different APIs I implement at work, but I only pull out a few attributes from each. I don’t want to go to the moon and back creating types for all this crap, I’ll have more than a 100 different types to model because of all the nested data and convoluted structures the APIs return.

I want the flexibility to incur tech debt where I want to, I don’t want the language to get in my way. That’s what makes JavaScript great!


In strongly typed languages, there are serialization/deserialization libraries that don't require you to type the entire JSON object - just the fields you care about.


I’m not a very skilled Haskell programmer, but every time I’ve tried to parse JSON in it I’ve felt a strong urge to switch back to Python or JavaScript. There are libraries that make it easier but I think it’s always going to be hardly to get started with JSON APIs in a language with a powerful and strict type system. Of course, that extra up-front effort might be worth it in the long run due to the benefits of type safety, but as the grandparent said, sometimes programmers need the freedom to incur technical debt.


I've really come to love strict JSON parsing, because everytime an API call was not as expected, I get the error straight at it's source, not somewhere 20 layers deeps in my views or models, way too late, and only with this one specific combination of events.


Aeson parsing seems straightforward to me, not sure it could be easier in any language. Serialization from your endpoint is automatic, based on your data types.

The real benefit of using a well typed language here is that you only need to parse the data once at this point, after that the type system makes sure the data is of correct format everywhere else in your codebase.


Aeson is not straightforward at all. Just try doing what the GP is saying he wants to do, you will discover you can't.

It is a very good library if you control both sides of the communication, and have fit for purpose interfaces. Otherwise, you are better with any lower level library.

Besides, what is it with the strictness of Haskell JSON libraries? There's nothing that will even accept Window's BOM.


You don't have to define a type for Json data in Haskell if you don't want to. Defining a full type is only norm/preference.

With Aeson, You always have the option work with a map directly and pull out properties by name.

That is also especially useful when you can't predict what keys or shape the data will have


I don't want to detract from Aeson. It's a great library. If you control both ends of your channel, it can be more convenient than the equivalent encoding and decoding on Javascript, and making a JSON library that is more convenient than JS is no small feat.

But it's a very complex library. It may not show at your code interface, but that complexity is there at the documentation and type errors. It is also famous for generating bad runtime messages. There is even another library for fixing this, at the cost of even more complexity.

If you fall outside of its optimum usage scenario, it won't afford you more functionality than a low level parser, so you are much better saving that complexity and going with the parser.


Ok, so use a parser then? Not sure what this has to do with comparison to dynlang/js.

OP was complaining that it's hard to type some kinds of payloads.

Sure you could write a custom parser for it.

With fromJSON in fact this is what you write. Maybe if you've only been driving Json instances generically, you never actually written a fromJSON instance in Aeson? I only use generic deriving in the simplest cases.. otherwise just write the fromJSON instance parser

My point was that you can defer parsing up front and just return a map of you don't want deal with the shape.

But sure you could write a custom parser as well, that falls more into the typed philosophy though than what js folks are used to


Fair enough, you're probably right, I have only used it in contexts where I build both sides of the application.


Out of curiousity, what exactly was the difficulty with decoding JSON in Haskell/


I was trying to use Aeson, which as noted in this excellent tutorial [1] (which didn’t exist when I first tried it) is “hopelessly magical for people who try to learn it by looking at provided examples, and existing tutorials don’t help that much.“ The rest of the tutorial should give you some idea of why I say that parsing JSON in Haskell is complex, at least if you’re used to doing it in JavaScript.

[1]: https://artyom.me/aeson


I'm interested in specifically what issues you had with JSON.


a quick search brings up a library, not sure why you would have trouble, other than maybe experience? https://github.com/bos/aeson


I was replying to someone who said they had trouble.


I found that by explicitly having to write types, the structures become a lot less convoluted. You split things apart, give them names, spot patterns more easily, and encode all sorts of information in a type system. And knowing that all fields are there when you need them is a relief.

Of course a code review can catch it. Of course careful programming can as well. But those are external constraints.

Having internal constraints greatly reduces the amount of mess one can produce.

Admittedly, sometimes to a point to which you’ve typed yourself in to a corner.


I have a library that is much like every other library in javascript. I decided to try TypeScript on it once as a test. The idea was to implement all the correct interfaces so that people couldn't call the wrong chain of methods. The interface list was almost the same size as my entire codebase.

I think TypeScript is essentially for retards. It is like "we need to lock everything down so the mediocre people we have to hire on mass can't break anything".


Typescript has the any type when you need it. You can write your code well typed but treat api returns as any if you wish. Although entering a type definition even for 100 types isn’t a big hassle, compared to day the code that has to do something with those objects. And will probably save the odd mistake so probably comes out ahead in terms of efficiency.



I agree but it's a big leap to lose Javascript interoperability to gain type soundness.

There are many such compile-to-js languages but none nearly as popular as Typescript is.


TypeScript already feels too much like C# for my taste.

But luckily with OCaml/Reason, there is a modern alternative to all these bloated C#/Java like type-systems.


If I point you to a language like that right now, would there be something else stopping you from using it? Like say, lack of typings? Or 'it's too niche'? Or 'I'm not comfortable with that style'?

Those are the things that keep people going to TypeScript.


I always think of it as a gateway drug that gets people into using other languages. Most fullstack developers I know are transitioning to things like Rust, Go, Swift, Kotlin, etc. Once javascript interoperability stops being a goal, there are many other languages that suddenly become attractive.

The irony with the Javascript ecosystem is that most of it is now written in typescript or other languages. That includes mainstream frameworks, tools, etc. Basically anything that matters to large numbers of developers. And of course for frontend development people don't actually ship a whole lot of third party dependencies in any case (for code size reasons). So our need for interoperability with all of that is actually pretty low. Most of the dependency hell that is NPM is really about pulling in layers of tooling that fix and work around each other in very odd and convoluted ways (i.e. webpack). React is a comparatively tiny framework of which several compatible even smaller versions exist that people tend to use when they don't care about supporting obsolete versions of javascript. What it does is pretty clever but you can recreate it in your language of choice with not too much effort (and people do this for lots of different languages). There are react inspired frameworks for Kotlin, Rust, and probably Swift, C# and a few other languages.

Typescript is pretty neat if you are coming from the giant Stockholm syndrome in our industry that is Javascript. It's held us captive by being the only choice for browser based development. Thankfully that wasted 2 decades of captivity is coming to an end now. So we can get back on track improving our tooling, languages, and practices. Things like refactoring and code completion are not exactly science fiction (both worked great in the nineties already). I think it's wonderful that e.g. VS Code allowed JS/TS developers to join this millennium two decades late but it's hardly the final answer in developer productivity. Time to move on.

IMHO, using Javascript these days is something to avoid. It's a compilation target at best and as such a stopgap solution until we can target WASM instead, which is possible now, will become very practical soon, and a mainstream/defacto thing to do soon after (2-3 years?). Most of this is pending on some ongoing work on fleshing out features for threading, memory management, apis, etc. Languages like typescript have a transitional role in the brief period of time that remains where transpiling to and interoperating with javascript is still better/easier/more convienient than compiling to WASM. Beyond that, unless you have a lot of javascript legacy that needs to be preserved/worked around, there are probably other languages that you might prefer to use instead. And as I argue above, there isn't actually that much code to rewrite if you consider what little of the NPM ecosystem actually ends up in your application on browsers. A few thousand lines of React code don't justify being held captive by that ecosystem. Also, most frontend code doesn't actually survive its first birthday in any case. So, what legacy?


I sometimes think wasm was invented to rid this glorious js ecosystem of people who always moan about it so the rest of us can get back to 'improving tooling and best practices'.


I still don't understand what pulls developers to Typescript. The examples given show major flaws in this (duck)type system. That it works when you don't use the 'any' type and when you know how to avoid all the edge cases and workarounds, I know. But when I see an example like this:

  const shasta = new Cat("Maine Coon")
  printDog(shasta)
  > Dog:Maine Coon

I see a huge red flag. Coming from C/C++ it looks like a joke. Why are all these front-end static type fanatics not more interested in Dart, Purescript, Elm, etc..?


Because you can get some decent typing with some compiler help AND leverage the entire node/js eco system super easily. Which is kind of the whole point.

As someone that's done c/c++/c#/lua/js/ts I've found typescript to be really awesome because it differentiates the types from the objects. Which has given me a much better appreciation of typing rather than the traditional OOP everything.


What do you mean by “differentiates the types from the objects”? Could you maybe offer an example comparing TS and C#?


in typescript I have something like

type theThing = { thing1: string, thing2: string };

and the actual object: const thing:Thing = { thing: 'hello', thing2:'more hello' };

"Thing" is not an object, I can't do Thing.new() etc. If I use a class in typescript it behaves just like you would expect C# / Java to.

This allows some really fun stuff with unions where I could do something like

const operateOnThings = (thing: Thing | Thing1 | Thing2) => { //do something based on what kind of thing it is }

Or I can build other types off of it like : type newThing = Pick<Thing, 'thing1'> & { newerThing: number};

Which is something I would have to do a lot of work with interfaces with in order to implement in C#. (I haven't done C# in a few years but I do love the hell out of it).


It just means that structural types are the default and you need to opt in to nominal types, when necessary: https://www.typescriptlang.org/play/?ssl=13&ssc=1&pln=13&pc=... (there are other ways as well)

In something like Java, it's the opposite: you get nominal types by default and you need to opt in to structural types (via interfaces).

Because TS is built on a duck typed language, tons of existing libraries would break if they had to declare explicit nominal types as arguments. Or you'd need to add custom interfaces for each library you use. Structural typing is what you want 99% of the time.


I think that Flow’s approach of using nominal types for classes and structural for everything else makes more sense. Are there really cases you expect classes to be structural types?


Objects in OCaml are all structurally typed.


Java interfaces are not structurally typed, are they?


No but it's the closest available. Two classes C1 and C2 that both implement interfaces I1 and I2 can be said to have a common structure. That is, if they both implement I1 and I2 then they have that structure in common.


Yeah but that’s still fully nominal typing, you can’t implement the interface implicitly, it has to be identified by name.


I've never used Typescript, but I don't think these are really 'major' flaws. That's just my perspective as someone that's being doing FE for over a decade and picks this stuff up fairly quick. Obviously it's more of a problem for newbs that probably aren't going to read that paragraph about discriminated unions and grok it straight away.

Having said that, I absolutely hate the ending of this blog post. I see blog writers do this all the time. He's just given three detailed, concrete negatives about Typescript, given enough information to infer that these quirks could be problematic for beginners and cost your team time and resources. Then at the end, he suggests that you use Typescript and waves it all away with some ambiguous bullshit about how static typing saves you a bunch of time.

There's so many of these claims that are just taken as common knowledge/best practices, that everyone treats as absolute truth that overrules everything else, even good concrete examples. I've been building entire apps myself for over a decade now, using both plain and typed (flow) JS, and every time I've seen somebody try and elaborate on how static typing saves you all this time, it's some pissant todo list bug that I would have solved in 5 seconds with plain JS.


Try working with a large 5 year old JS codebase that's been hammered on by dozens of devs. The problems are no longer just pin point bugs but a systemic lack of modeling.

Static typing is not that much about preventing bugs but more about forcing the code to operate over a well defined model.

Can you write good code without static typing? Yes. Can you scale and maintain it in a large organization, probably not.


It’s structural static typing, a perfectly cromulent class of type system, and much more easily compatible with dynamics types.


Structural typing and actual first class union types in typescript are better than basically all the ML-based languages.

Dumb example: imagine you had

Animal = Dog | Cat

Person = Plumber | Driver

If you want a list of “stuff” in your system , in typescript you just say you have Person | Animal.

In ML languages you would need to introduce an Either type so you’re working with Either Person Animal.

So now in your code you’ll need to introduce a bunch of Lefts and Rights. And if you end up needing to compose you’re introducing even another layer of Lefts and Rights. And you don’t get much of any or the inference capabilities of TS to just do this for you.

ML languages don’t have first class unions, they have ADTs, which introduce difficulties and lead to classic type safety weirdness like “why can’t I write a function that only accepts one ADT variant??”

Typescript resolves a lot of these, do you end up with a much better local maximum for typechecking IMO.

Typescripts type unsoundness means you unfortunately can’t really implement return type polymorphism though...


What you are describing is still an ADT. It's just a different syntax.

Indeed, your examples of the TS syntax are more convenient than ML, but that is reversed when going the other way, and decomposing the types into smaller sums instead of composing.


> I still don't understand what pulls developers to Typescript... Coming from C/C++ it looks like a joke.

Remember that this all compiles down to JavaScript where it's all dynamic/weak types. Someone coming from C/C++ should definitely appreciate TypeScript over just plain JavaScript.


Because TypeScript is a superset of JavaScript and works directly with existing JavaScript code.

It’s the same reason that there is any interest at all in MyPy for Python, Sorbet for Ruby etc.


Why not embrace Elm or Purescript?

Because those are a massive paradigm shift. TS is easy. You can sell it as “JS but better. If all else fails you can always resort to any”. Selling Elm to someone who only ever used C# is very hard. I’ve tried.

It is also familiar enough for the hordes and horses of C# and Java devs in your company. It’s even an MS product. Enterprise yay!

A safe choice. You won’t be crucified if it goes wrong.

It’s sad, I know.


TypeScript does not offer perfect typing, but on the other hand, your example is wild in such a way, that I don't think I would ever encounter it with a professional team of developers.

I.e. it would be quickly caught in a review, and the offending developer would have to bring cake the next day.

Yes, TypeScript is not perfect, but we take what we can get


Being able to gradually introduce types into an existing code base seems like a poor reason to use unsound or quirky features in a language. I’d then much rather use a small and consistent language if I’m going to compile to js anyway, even if it’s not useful as a migration path.

What seems like a good reason for accepting language design tradeoffs on the other hand is the package ecosystem. A language that doesn’t work seamlessly with existing third party js is probably doomed. It’s not too difficult to write a tiny language that is nice and consistent and compiles to js, but few will use it if you need ffi to interact with js code. The impressive engineering effort in TS is all about these tradeoffs.

This is why I’m having a hard time loving TypeScript, it’s a language for solving a real world problem (bringing types to the js ecosystem) in a pragmatic way, and that rubs me the wrong way. It acknowledges that js, js developers, and js packages are not going to going away any time soon.


Ok since we have a lot of TS people here maybe someone can help me out.

Let's say I have a simple interface with a few fields. I want to make sure incoming JSON conforms to that interface.

I realize at runtime the interface isn't there, but is there some tool I can use to compile a utility to check JSON against an interface without having to write a duplicative JSONSchema or something else?


io-ts [1] is what I think most people use for this

1: https://github.com/gcanti/io-ts


Personally I dislike the fact that io-ts requires taking a pure functional programming approach to this validation. For most cases I just want to throw an error if something doesn't match up. My main use case for this is validating that my UI and backend API have the same ideas about the shape of the data, which will either always work or always fail. All errors should be caught during development and testing, so the "either" case will never realistically be hit in production.

Because of this I prefer runtypes [1], because it's much more simple to get my desired behavior. My only gripe is that errors aren't all that descriptive.

I guess I could write a function to pipe/fold into the type, throwing a descriptive error if something fails, but I like the simplicity of runtypes.

Edit: I just discovered io-ts ErrorReporter [2]. That's way better than my solution, and I'm considering switching now!

1: https://github.com/pelotom/runtypes

2: https://github.com/gcanti/io-ts/blob/master/src/ThrowReporte...


We wrote a bunch of utility methods around io-ts at work with custom error reporting/handling, otherwise it can indeed feel a bit too fp-/boilerplate-heavy for your every day frontend development, especially in combination with other stuff such as rxjs/redux.

But once you have those set up, it's an easy breeze, and the benefits are enormous. Our helper-method with the hopefully self-explanatory name "validateOrThrow<T>(t:TypeGuardForT)" is essentially called at every point where there is some form of incoming external data, and the amount of debugging-headaches that have simply disappeared because of this is mind-blowing.


My advice is bite the bullet and just write a JSON Schema for the interface and use ajv[1] to check it. Keep the interface, the JSON Schema, and the validator function (which should be a type guard[2] to take full advantage of TypeScript) in the same module for easier maintenance.

[1] https://github.com/epoberezkin/ajv

[2] https://www.typescriptlang.org/docs/handbook/advanced-types....


I am having the same problem and I think I am going to go with json schema.

The problem is that a lot of the things you want to validate aren't easily expressible as typescript types (e.g., valid email address, make sure two fields are always the same length, etc). If json schema are more expressive you want to use that as your source to generate the typescript interface instead of the other way around.


I solved this using the ts-interface-builder and ts-interface-checker libraries and have been happy with the result. You write a plain TS type (rather than a custom syntax like io-ts has) and then run a codegen step to make a runtime representation of the type. Then you can create a checker from that and do runtime type checking.

https://github.com/gristlabs/ts-interface-builder

https://github.com/gristlabs/ts-interface-checker

Example: https://github.com/alangpierce/sucrase/pull/468/files


I had the same problem and used a JSONSchema as a single source of truth. Then from that schema you can generate:

- the typescript interfaces: https://www.npmjs.com/package/json-schema-to-typescript

- the runtime checks with AJV

So you have no duplication and you're type safe.


We use TSOA[1] for that, it can build OpenApi specification from your ts interface. So its could be used for documentation and tsoa can validate but other tools can validate too

[1] https://github.com/lukeautry/tsoa


I’d suggest generating code

Write your interface in json or text proto then build it to ts interfaces and runtime checks.


Agree with the code generation approach. Otherwise you need to make sure the annotations are in sync with the type / class / interface.

Typescript doesn't support macro currently, so code generation is the only way to programmatically create 'type-checked' code.

Some cli-tool and library I'm using (available on npm):

tsc-macro: https://github.com/beenotung/tsc-macro

gen-ts-type: https://github.com/beenotung/gen-ts-type

ts-type-check: https://github.com/beenotung/ts-type-check


I wrote a tool that may be useful here; it generates a TypeScript definition file and runtime type checking logic given examples of the objects you want to accept.

https://jvilk.com/MakeTypes/


I've used typescript-json-schema to generate a schema based on an interface. I had a script that could regenerate the schema, and a CI script that ran ajv on a set of fixture JSON files to ensure any schema changes were backward-compatible.


ts-json-schema-generator[1] generates a schema from an annotated interface definition.

[1] https://github.com/vega/ts-json-schema-generator


Let me be honest, but this article is just horrible. The whole article is for the sake of complaining (though I prefer to call this "nitpicking").

> 1. Interfaces with excess properties

... in immediate objects, which will never be reused. This is nothing worse than golang emitting errors on every single unused variable. There are good reasons to behaviours like this, even though not always preferable.

> 2. Classes (nominal typing)

It seems like the author straight rejects the idea of structural typing itself, by calling the concept itself a "quirk". This is rather about preference, not right-and-wrong. If this point is to be true, one should also reject duck typing, many other popular dynamic languages, a large portion of software industry, scientific researches, etc. Good luck with that.

> 3. Discriminated Unions

While I do agree that it's a shortfall of TS type system, the example is simply unrealistic. Using composite values as type discriminator is a bad design. Plus, the compiler is not even allowing anything destructive nor bug-prone.


I don't want to go into detail, but everything the author said is true. These are real quirks, that aren't explained correctly in the documentation.

They are all justified, and the author acknowledges this, but nevertheless surprising.


After using TypeScript for the past couple of years, I've come to see it as simply another "embrace and extend" maneuver by Microsoft carve off a chunk of JavaScript mindshare, preying upon the naïveté of less-experienced developers who've fallen for the "static typing is safer" myth. TypeScript's decision to disallow JSDoc within .ts files [1] for example doesn't contribute anything positive to my impression of this syntactic pocket-protector. While the type hinting it provides is useful, the tradeoffs of added complexity, tool dependencies, and cross-compiling phase have made it more trouble than its worth in my opinion, particularly since tooling to provide code introspection and type hinting has already existed for a long time [2]. For new projects, I've begun using straight JSDoc, which is working beautifully with minimal setup [3] and provides all of the benefits I ever found useful about TypeScript, while remaining completely unobtrusive otherwise[4].

1. https://github.com/microsoft/TypeScript/issues/20774

2. https://ternjs.net/

3. https://medium.com/@trukrs/type-safe-javascript-with-jsdoc-7...

4. https://fettblog.eu/typescript-jsdoc-superpowers/


The first example doesn't bother me and I think it's a weak argument. TypeScript allows literals[1] as types[0] so it is trying to use the object as a type.

The correct use of:

  printDog({
      breed: "Airedale",
      age: 3
  })

is to explicitly cast it:

  printDog({
      breed: "Airedale",
      age: 3
  } as Dog)

[0] https://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&p...

[1] https://www.typescriptlang.org/docs/handbook/advanced-types....


Casting can hide errors. It's better to assign it to a variable first that has a type declaration.

    const dog: Dog = {
        breed: "Airedale",
        age: 3
    }
    printDog(dog)
An IIFE is safe too:

    printDog(((): Dog => ({
        breed: "Airedale",
        age: 3
    })())


I would absolutely reject a PR with the IIFE.


Actually this cast is safe in TypeScript, additional variables are unnecessary.

The statement ‘const d: Dog’ and the expression ‘d as Dog’ are equivalent w.r.t. type safety. The compiler will error in both cases the if the value is not a Dog.


This is not correct. Following example is valid in strict TypeScript:

  interface Dog {
    breed: string;
  }
  const dog = {} as Dog;
  console.log(dog.breed);
Using `as` is very unsafe in TypeScript.

I've collected more quirks of `as` in this playground: https://www.typescriptlang.org/play/index.html#code/PTAEAMEs...


Yeah and both of those are less readable, and used everywhere add up to a really crap code base relative to the same code in plain JS.


Yah, pojs is so much nicer because when you mistype breed as bred you get to debug the issue at runtime for a while changing things that are irrelevant when TS could have told you immediately


The article is just describing quirks, not arguing against them. Specifically for the first example they said "I think that the TypeScript stance here isn’t wrong".


Casting is as bad as using "any". You can make the compiler think anything you throw at it as that type and you can get bitten at runtime.


Thats not 100% true in TypeScript. TypeScript actually only allows "up-casts" and "down-cast" (but if the type is neither a supertype nor a subtype the compiler will throw an error).


Ya, I often have to write “this as any as T” because I’m using T as a self type.


That’s my understanding too. You just can’t cast types wily Nily.


Nope. This is not how casting works in TypeScript. To get an unsafe cast you have to go via the ‘any’ or ‘unknown’ types.

e.g.

‘foo as unknown as Dog’


There are a few casts that are allowed that I would consider unsafe, particularly around unions of literals or using empty object literals:

  // No error here
  type Animal = 'dog' | 'cat';
  const notAnimal = 'couch' as Animal;

  // No error here either
  type Food = { isSpicy: boolean };
  const empty = {} as Food;

https://www.typescriptlang.org/play/index.html#code/C4TwDgpg...


Casting may hide errors and is unnecessary in this case: `as const` will work just as well.


The correct use is to remove the `age` property in this case: if you're not keeping a reference to the object or that property and the called function cannot use it, why have it in the first place?


That’s not always true. If you are going to print the object to the console or send it in a network request it’s possible that the additional property is important.


But then it should be in the type signature of the object. TypeScript is signalling this as an error because it's a literal passed directly to the function and that property (theoretically) can never be used because it wasn't declared.


Why would casting hide errors?


In the simple case, if it could be Dog|Cat and you cast as Dog but you get a Cat, and TS could have told you about the error if it hadn't been forced to treat it as Dog. This can be fixed by having your types be derived by runtime validation.


>> Real world JavaScript is inconsistent, messy, and complicated

JavaScript is consistent. Expressive languages such as JS are consistent in their permissiveness. It doesn't stop developers from doing irrational things like comparing objects with numbers just like TypeScript doesn't stop developers from writing all other kinds of flawed logic.

TypeScript basically only protects code from typos. The worst bugs I see in production systems are almost never caused by typos or by comparing incompatible types; usually they're caused by issues in the flow and manipulation of data; for example the same instance is being modified in two different parts of the code without the other part of the code being aware of it; or in the case of pure FP a function is using outdated instances because a change in the underlying data source did not propagate through correctly, etc...

TypeScript is to a software developer what a spell checker is to a book author; it's convenient but once you run the final product past your editor-in-chief (whose analogy in the software world are unit/integration tests), the spell checking didn't actually add any value; at best it saved your editor-in-chief some time. It has no bearing at all on whether or not you'll get a Pulitzer Prize or a top ranking on Amazon.

The downside to TypeScript is compile time and this is a significant drawback IMO because it slows down iteration time significantly. Time spent waiting for the build to finish is time that was not used to add new tests and new features.

If you add up all the time that all developers on a project spent on waiting for the build (or lost their focus/train of thought as a result of waiting for the build to complete), how many extra unit or integration tests could have been written using that lost time? I think if you do the math, you will find that TypeScript is a net liability to the project.


You obviously never wrote TypeScript with strict flag turned on. It does control flow analysis, not just typo checking.


It does 'Control Flow Based Type Analysis' - Meaning that it only analyzes control flow for the sole purpose of determining type correctness which adds almost no value to the project.

It doesn't prevent asynchronous control flow issues like for example;

- Having two different parts of the code simultaneously (asynchronously) mutating the same object and causing data inconsistencies.

- Your code starts a new asynchronous job (e.g. setInterval) in the background without killing the previous/existing job which was launched earlier.

- An event listener was registered for the same event multiple times (e.g. from inside another event handler) without unregistering the previous listener and so every event now triggers multiple updates instead of one (and CPU usage keeps going up and you have a memory leak).

- It doesn't tell you when you forgot to trigger a specific event

- Or guarantee that two different parts of your code never run in parallel (asynchronously) when mutating some state.

- Or that you missed an edge case and forgot to update a specific instance's property

- Or that you were modifying the original instance of the object instead of merely a copy/clone of it as you assumed (or the opposite was necessary and you made the reverse assumption)

Only a human brain can prevent those issues (and the list of such difficult issues is practically infinite; my list is a tiny sample) and those are the real issues in software development, not typos and auto-completion.

Unless you're a web developer and your job only involves writing static webpages, you are likely to encounter much more difficult problems in your career besides not having automatic method name completion or typo prevention mechanisms. These problems I described above are complex enough that they make all the problems solved by TypeScript seem negligible. TypeScript's compile time delay becomes a bottleneck when trying to debug and resolve real difficult problems.


> the list of such difficult issues is practically infinite

Precisely. No matter how good a language is, you can always say "but it doesn't do xyz". If you want to replace the human brain by a language, then you're looking for AI, not a language.

I understand what you mean, and I agree that these things are lacking from TypeScript. And I would love them. TypeScript is slowly adding more and more things, mitigating issues one by one, which were previously dissed off by developers with "it doesn't even do xyz".

I have my job because my brain is smarter than TypeScript's compiler. And it does help me, because I know which things I don't have to think about anymore, based on how I write the code. So it does help me, because I think less about the set of problems which it DOES solve.


There are problems in their approach:

In the first case the behavior is quirky because they are passing an untyped object into a function. If the object is typed the behavior is consistent.

The second example is about classes. Classes are created as easy extension objects via inheritance and poly-instantiation. Those ideas are friendly concepts for many developers but they increase complexity so I intentionally avoid classes and use a custom ESLint rule to enforce such.

In the third case they can solve for complexity with a simple refactor. The two interfaces are structurally identical so they should be a single interface with either a union on property values (static literals) or by defining a property again a union of other interfaces called by reference.


It can only be more complex, then be simple.

If Typescript never existed, people got used to simple nominal typed languages like Java would never notice there are much more advance type system ahead, and what can it potentially bring us - the lambda cube, the dependent type, the Curry-Howard correspondence.

After everyone have a much better understanding in type and the complexity in Typescript, simpler and more powerful languages would take off.


Regarding #2, if the class has any private properties (even if the classes have the same private properties) they are no longer assignable.


How does TypeScript ensure encapsulation if it doesn't have nominal types?

Edit: The article says "magical hidden properties".


Programming TypeScript has a section on “Simulating Nominal Types” [1]. You define a type that’s impossible to create naturally, and provide a function that asserts something is of that type.

    type CompanyID = string & {readonly brand: unique symbol}
    type OrderID = string & {readonly brand: unique symbol}
    type UserID = string & {readonly brand: unique symbol}
    type ID = CompanyID | OrderID | UserID

    function CompanyID(id: string) {
      return id as CompanyID
    }
    ...
TypeScript can be very confusing sometimes, and given the learning curve I’d caution someone trying to learn modern JS away from starting out with it. But in even small codebases it’s an amazing improvement in safety, especially for refactoring.

[1] https://learning.oreilly.com/library/view/programming-typesc...


> TypeScript can be very confusing sometimes

That's not good. A type system should be crystal clear, predictable and reliable.

> and given the learning curve

Types should not be a complex thing with a steep learning curve. If you know Assembler and C it's pretty clear what types are about. Added complexity to your codebase is what you should try to avoid at all times. We have a limited capacity of things we can think of at a time, developers should therefore focus on things that really matter. In my daily work I see highly complex piles of spaghetti in TS that are 'type safe' and easy to refactor..


I don't follow how knowing C and Assembler makes types clear and I'd love it if you could clarify.

Nominal types are really a compile time only thing: if you have two structs (or classes) with the exact same fields, I'd expect them to be stored the same way in memory. And as a result I'd expect any function to work on them just fine, as long as they find semantically valid data at the right offsets.


> That's not good. A type system should be crystal clear, predictable and reliable.

The purpose of TypeScript is to provide static types to JavaScript without just turning it into a compile target. TypeScript type system is only confusing because it's modeling real-life ultimately type-less JavaScript code. It's not it's own language with it's own rules -- it lives and dies as JavaScript.

> Types should not be a complex thing with a steep learning curve.

Types can be as simple or complex as needed. Assembler and C have very simple type systems with very few features and can only model very simple situations. Types in TS don't have to be complicated -- blame JavaScript programmers for their crazy designs.


At runtime, TypeScript is JavaScript, so depending on your perspective either encapsulation is impossible (because you don't have types to defend you) or easy (because you use the patterns that JS has to defend encapsulation at runtime, without types).


#private is coming soon, so that won't really be true any more. https://github.com/microsoft/TypeScript/pull/30829

You'll have class members with accessibility in their declaration, and real run-time encapsulation.


You can use private to do it OO/Java style, but you don't need typescript for that: ES6 modules let you encapsulate stuff just fine without any type features.


At compile time? It's very simple–use an interface and provide constructor functions that upcast object instances to the interface. Now, only the interface-defined members i.e. the public members are accessible.

At runtime? You can't, it's JavaScript, everything is public (for now at least).


You can hide some of your structure using "private". There are also branded types: https://github.com/spion/branded-types


After using Clojurescript for some time, I don't even understand anymore what kind of problems Typescript supposed to be solving. Sometimes it feels it causes more headaches, to be honest.


From the HN discussion about the video of "A Conversation with Language Creators: Guido, James, Anders and Larry"

https://news.ycombinator.com/item?id=19568378

https://www.youtube.com/watch?v=csL8DLXGNlU

I posted these Anders Hejlsberg quotes, who co-designed TypeScript, C#, Delphi, Turbo Pascal, etc:

https://news.ycombinator.com/item?id=19568378

>"My favorite is always the billion dollar mistake of having null in the language. And since JavaScript has both null and undefined, it's the two billion dollar mistake." -Anders Hejlsberg

>"It is by far the most problematic part of language design. And it's a single value that -- ha ha ha ha -- that if only that wasn't there, imagine all the problems we wouldn't have, right? If type systems were designed that way. And some type systems are, and some type systems are getting there, but boy, trying to retrofit that on top of a type system that has null in the first place is quite an undertaking." -Anders Hejlsberg

>Andrew Hejlsberg:

>Maybe I'll just add, with language design, you know one of the things that's interesting, you look at all of us old geezers sitting up here, and we're proof positive that languages move slowly.

>A lot of people make the mistake of thinking that languages move at the same speed as hardware or all of the other technologies that we live with.

>But languages are much more like math and much more like the human brain, and they all have evolved slowly. And we're still programming in languages that were invented 50 years ago. All the the principles of functional programming were though of more than 50 years ago.

>I do think one of the things that is luckily happening is that, like as Larry says, everyone's borrowing from everyone, languages are becoming more multi-paradigm.

>I think it's wrong to talk about "Oh, I only like object oriented programming languages, or I only like imperative programming, or functional programming".

>It's important to look at where is the research, and where is the new thinking, and where are new paradigms that are interesting, and then try to incorporate them, but do so tastefully in a sense, and work them into whatever is there already.

>And I think we're all learning a lot from functional programming languages these days. I certainly feel like I am. Because a lot of interesting research has happened there. But functional programming is imperfect. And no one writes pure functional programs. I mean, because they don't exist.

>It's all about how can you tastefully sneak in mutation in ways that you can better reason about. As opposed to mutation and free threading for everyone. And that's like just a recipe for disaster.

And these Larry Wall and James Gosling and Guido van Rossum quotes:

>James Gosling wants to punch the "Real Men Use VI" people. "I think IDEs make language developers lazy." -Larry Wall

>"IDEs let me get a lot more done a lot faster. I mean I'm not -- I -- I -- I -- I -- I'm really not into proving my manhood. I'm into getting things done." -James Gosling

>"In the Java universe, pretty much everybody is really disciplined. It's kind of like mountain climbing. You don't dare get sloppy with your gear when you're mountain climbing, because it has a clear price." -James Gosling

>"I have a feature that I am sort of jealous of because it's appearing in more and more other languages: pattern matching. And I cannot come up with the right keyword, because all the interesting keywords are already very popular method names for other forms of pattern matching." -Guido van Rossum

Also:

https://news.ycombinator.com/item?id=19568860

DonHopkins 10 months ago [-]

>Anders Hejlsberg also made the point that types are documentation. Programming language design is user interface design because programmers are programming language users.

>"East Coast" MacLisp tended to solve problems at a linguistic level that you could hack with text editors like Emacs, while "West Cost" Interlisp-D tended to solve the same problems with tooling like WYSIWYG DWIM IDEs.

>But if you start with a well designed linguistically sound language (Perl, PHP and C++ need not apply), then your IDE doesn't need to waste so much of its energy and complexity and coherence on papering over problems and making up for the deficiencies of the programming language design. (Like debugging mish-mashes of C++ templates and macros in header files!)


To me, TypeScript is "perfect". Unlike other strictly typed languages (looking at you OCaml, ReasonML, Haskell), TS has never prevented me from achieving all three of:

1) Be type safe enough to guarantee practically bug-free code (at least with regard to types, you can always have logic bugs)

2) Actually achieve what I want to do in less than a day

3) Upgrade easily when a new version of the language/tooling is released

The only thing that's missing is indeed better nominal typing so that we can more easily discriminate between logically separate instances of strings and numbers. For objects it's actually good enough to qualify for "perfect".


TS has led me to a desire for more functionally pure typed code. I'm super interested in learning OCaml and ReasonML right now so I would be interested in understanding where you felt ReasonML falls flat vs TS.


I definitely recommend trying them, as it will be a great learning experience. It will probably even make your TS better.

What you won't be able to do is to actually ship a product easily, as the ecosystem is virtually non-existent. Documentation - virtually non-existent especially for actually creating working products.

Of course, there are exceptions to this (like the Facebook Messenger being written in ReasonML), but people tend to underestimate what kind of engineering effort went into that. If you actually want to do stuff by yourself, use TS.


And that's the funny bit because a lot of the quirks in TS are actually bits that are needed to integrate with other imperfect tools and libraries and deliver a fully functional product.


TyspeScript is nothing more than just better type annotations for JavaScript


Sounds like we're about to see a book titled "TypeScript - The Good Parts". That was actually one of my favorite JavaScript books and led to my early adoption of CoffeeScript (since it purposely fixed the "bad" and "ugly" parts at the back of the book.


Well, typescript is constantly improving over the years, witch each release you are able to represent more real-world JS patterns and type system is getting more strict options. So this 3 points from article might be improved in future.

About the point the article makes:

1) I remember when excess property check was introduces I was really happy! Because I remembered bug that would be caught with it. I had some interface with optional properties in common library and I renamed property name. Of course when I updated this library in another project I didn't get any compile errors because this was optional property and now I was just passing some extra property which was ignored. Excess property check would have found it because I was passing object literal.

Structural typing is great choice in Typescript because it's basically type-safe duck-typing which is used everywhere in Javascript projects. It's not uncommon to have one component that accept objects with some properties but this object contains many extra properties because it came from different component and these properties are used elsewhere. It's standard pattern in Javascript world. But when your function requires some object of given shape and you are creating this object right now using literal syntax, then it's really likely that you made a mistake. Why create extra properties for just created object when you clearly see they are additional? And I read that Typescript teams is very happy with this feature[0].

However, excess properties check is great but useful only on call side. I cannot say that my function doesn't accept extra parameters. I would really love to get Exact types[1]. Which would allow me to force not having extra parameter in passed object.

2) As mentioned in article, Flow decided to have classes nominally typed. In Typescript everything is structurally typed so it's actually more consistent ;) I really hope we'll get nominal typing and opaque types sometime in future. I read TS team was thinking a lot how to do this. I don't remember what is the current status of this.

3) For me it's more problematic that Typescript doesn't narrow types of an union when you do conditional check. The reason is that in general it is unsound (but I think it would not be if you had only union of Exact types, that's another reason why I would love to have Exact types!).

Example in article doesn't have this problem so I guess this could have been improved. Sound like minor issue for me but maybe author could create suggestion on github to improve it and allow discriminator to be nested?

[0] https://github.com/Microsoft/TypeScript/issues/12936#issueco... [1] https://github.com/Microsoft/TypeScript/issues/12936


I like how nobody has realized that typescript is classic Microsoft embrace, extend, extinguish. Oh no, they have "turned a corner". This is a different Microsoft! Haha.. they are exactly the same. Every single company has wanted to try and control JavaScript as it takes over more and more of the roles other languages used to fill.

Google failed with their new language, Microsoft is having some success. Google claimed "javascript was too slow, so we need Dart". Hmm, turns out to be complete bullshit and electron applications are now the standard way to create desktop applications. Microsoft is claiming "you need static typing", which is more bullshit.

First Microsoft embraced JavaScript, then they made TypeScript to extend it, and now there is talk of "hey why not just use c# with web assembly etc". "why not just make deno TypeScript only". "what if Chrome only ran TypeScript, would it be faster?".

With vscode->typescript->github, Microsoft is managing to gain control of the masses of mediocre developers, while the good programmers will always be elusive. A digital divide is getting bigger.


The only electron app I've ever used that wasn't slow/terrible is VSCode which happens to be written in typescript by Microsoft and it still occasionally decides to use 100% of my macbook's CPU making it entirely unusable until I restart, so I'm not sure that's a very good argument.


Do you think that's actually thanks to TypeScript? From what I know, TypeScript doesn't have an inherent performance quality that makes it better than JavaScript.


No, typescript has no impact on runtime performance that I know of.




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

Search: