Hacker News new | ask | show | jobs
by cryptica 2560 days ago
I've been using TypeScript for over 1 year after 10 years of JavaScript and I really don't like it. It slows me and the team down and creates more problems than it solves. It doesn't even ensure type safety because there is no runtime type validation for JSON objects received from the API. It's disturbing that so few people can see how useless it is. To me, it's extremely obvious.

TypeScript is a hack of epic proportions and every so often the reality rears its ugly head in the form of failed source mapping, version compatibility issues, unexpected types during runtime, poor architectural decisions aimed at pleasing the compiler instead of fulfilling project goals.

TypeScript is a very poor way to model real-world systems because it incorrectly assumes that real-world entities have a fixed type schema. This is obviously wrong. Real-world objects change over time and most things cannot be categorised clearly. A tadpole turns into a frog and learns the ability to walk. Some people are disabled and cannot walk. Some cars have 6 wheels. There is no fixed type schema for anything in the real world; it is filled with anomalies so why should we design systems to model such unrealistic objects? Why not force the developers to account for as many cases and schemas as possible, it's our job!

11 comments

As someone who has also been using TypeScript for over a year and JavaScript for over 10 years, I've had a different take on it.

Disclaimer, I freaking love JavaScript and TypeScript.

> TypeScript is a hack of epic proportions and every so often the reality rears its ugly head in the form of failed source mapping, version compatibility issues, unexpected types during runtime, poor architectural decisions aimed at pleasing the compiler instead of fulfilling project goals.

This just sounds like JavaScript to me. Maybe TypeScript has helped elucidate those problems for you. TypeScript adds some syntax to help formalize solutions to these problems that existed in JavaScript already.

> TypeScript is a very poor way to model real-world systems because it incorrectly assumes that real-world entities have a fixed type schema

This sounds to me like programmer error. TypeScript provides facilities for unknown structures, or structures that have multiple possible definitions. There is the union type for which we can tell the compiler that a value can be of type `A[ or B[ or C[ or... ]]]`. Perhaps even more powerful is optional types. In interface you'd describe an optional field with a question mark, like `{ foo?: string }`. Alternatively, you could use the union type described above, unioned with `undefined`. These structures help you create types that actually do model the uncertainty of data coming into your program. If you're not using these techniques, then you're missing out! Having the type-checker yell at me because I didn't check for the undefined case is a really really cool thing.

> Why not force the developers to account for as many cases and schemas as possible, it's our job!

I would argue that TypeScript provides the vocabulary for doing just that and that JavaScript provides no such niceties. Sure, you provide run-time checks on a piece of datas validity, but there's no system to check that run-time behavior before code gets merged to master.

If you're not running `"strict": true` and annotating your TS code with optional values when data is coming into your program, you might have a bad time.

> It doesn't even ensure type safety because there is no runtime type validation for JSON objects received from the API.

It ensures internal type safety. If you don't have a lot of complexity on your side (e.g., a large client-side app) that may not be worthwhile to you, but it's certainly a thing it does, and a reason static typing is generally considered to be of value, particularly in large, multiprogrammer and, even moreso, multi-team projects.

Typescript has been useful for my team to reduce the number of unit tests and constant checking of number and order of parameters in functions, presence of null/undefined values, etc - when compared to JS obviously. It works particularly well when coupled with JSON schema validation that checks that the contract of your api is not ignored (TS types map very easily to and from JSON schemas).

As for your third paragraph, we follow some degree of functional programming precepts, which implies we avoid mutability as much as possible, so I can't comment much on the problems of mutating TS. I enthusiastically recommend pursuing immutability though, TS or not.

I still don't get it. You can also add rigorous schema validation to your API endpoints with plain JS so you don't need to worry about strange inputs in your tests either. This is not unique to TS.
Perhaps I didn't explain it well - my point was not that you need TS to validate your inputs, it was that you can use TS to guarantee internal consistency of data, and that said internal consistency will work well as long as you ensure that the input is safe -which you do need to implement through other means like json schema. What TS offers in this context is to ensure that you didn't call a function with arguments present in the wrong order, that you didn't assign one variable instead of another, that you didn't forget to include one last argument...

All things that come up relatively frequently in vanilla JS during refactoring processes.

Yes, but it feels like more work and it's more verbose. In Typescript, it's also more or less convention, where as in JS it's just nice to have. I have less guesswork to do with Typescript. There's value in that.
It doesn't sound to me that you really took it seriously and try.

For example for what you said about "JSON objects received from the API" has no type validation, it doesn't unless you define the types for it. You can write interfaces that defines the API responses and use it across whole code base.

This is incorrect, merely declaring a type for all your API inputs does not prevent clients (especially a bad actor) from actually sending objects of different types at runtime; and when they do, your code will crash catastrophically. Try it.

You need to do manual schema validation, just like you did with JavaScript. TypeScript adds 0 value. The type only gives you an illusion of safety, which is worse than no safety at all.

"Types are useless because they don't version my wire protocol!" is a wholly specious argument. There are a variety of RPC protocols that address versioning in a variety of ways; this isn't a typechecker responsibility. You can generate Typescript interfaces from protobufs if you fancy.

Typescript makes sure that all your code agrees with the protocol definition you have chosen. That alone is immensely valuable.

Protobufs is an antipattern. You can't expect external systems to agree with your system's type definitions. It just gives other people extra work when integrating with your system.

JSON is better because it allows external services to interpret the data in their own way regardless of their type system.

Typescript only checks all that at compile time though. The complaint here is that even a typed json.Parse<T> :(json:string)=>T will not actually give any type checking when you pass it a string containing any schema.
When you type json.Parse<T> you are taking on the responsibility that what is returned is actually T. If you want the type checker's help that you are sure it might not be T, then ask for it: json.Parse<unknown> or json.Parse<Partial<T>> is maybe more accurate, and would force you to write runtime checks. Partial<T> in particular is a great helper type that is often exactly what a lot of want in a json.Parse situation: gives you the field autocomplete you want, but reminds you that they may be undefined.
A sound type system shouldn't allow you to write such a parser function without either you lying in its implementation or having to use unsafe code.

In a year or two, once everyone has switched to use `unknown` instead of `any` in functions which at runtime return… uhm… unknown values, we'll all be much safer. Because we'll be forced to use proper parsers and validators.

And that's what `runtypes` provides.
Used properly, runtime checking and types go well together. Types help you remember whether incoming data has been validated. You can use them to make sure your code does runtime checking exactly once, rather than multiple times or not at all due to changes in different packages that can introduce security bugs. This can make security reviews a lot easier.

I don't know if that's commonly done in Typescript, though? It seems more common in web apps to trust your own server to send you good data.

There are libraries that let you define runtime schema objects which you can invoke to validate incoming data — and they also let you extract static types from those schemas so you don’t have to write things twice:

https://github.com/gcanti/io-ts https://github.com/pelotom/runtypes

"Typescript adds 0 value" to taking user input. Sure. I mean, it's kinda missing a lot of nuance--especially given that it's kind of smart to let-it-crash universe where bad-actor input making your handler crash or fail, so long as it does so safely, is not the end of the world--but yes, you do in fact have to validate user input. You have to do that in every other language too, of course, and stuff like `class-transformer` or `runtypes` or `io-ts` exists to make it easier and safer, but yes. You do.

What about module boundaries between code? What about even the basics of knowing what you're passing into a function is correct?

Me, I write a lot of TypeScript. I've never written code on top of the Node virtual machine as quickly or good or as correct because I'm not stuck resorting to nonsense tests around "well, what do you do if you pass the wrong type to this function?" and I'm not validating internal arguments because TypeScript told you if it was wrong, this is not my bug. Instead I validate user input at the edges (less because of "bad actors" and more to provide helpful messages to the consumers of my libraries and APIs) and then I have a snappy and reasonably correct compiler yelling at me when I do something wrong, immediately after doing so. And because my entire ecosystem, past that scary user-input edge, is also in TypeScript, I am much more sure of the code I'm writing. And, as mentioned, I write it way, way faster.

So if you care about correctness and code quality, why is "well, input validation on the edge is harder" so much more important to you than that sort of thing?

People should not be downvoting this.

There's a weird impulse out there to try to pretend the limitations of static type-checking don't exist... namely, that it's static. That is, it's build-time checking and doesn't provide guarantees at runtime.

For tightly coupled systems were you control all tiers, you can stretch the value of static type checking by ensuring that changes to back, middle, and front all roll out together. But obviously, you're seriously limiting the scalability of your product and agility of your releases when you do this (you need a command-and-control dev communication structure). If you want static type checking to make guarantees beyond the local component, this is a major architectural commitment. To maximize the "guarantees" of static type-checking, it will affect the topology of your entire product, including the release process. (How much downtime is acceptable per release? What limitations are you willing to accept in terms of distributed scalability? Is it acceptable that user sessions become invalid for a release and how will you handle the UX for this?)

Not saying you shouldn't do it, but go in with your eyes open.

Now, static tools certainly have their uses. But:

(1) type-checking is just one limited case. e.g., go get eslint, turn on almost all the rules, and work with that for a few weeks. After you get over the initial shock, you'll get a lot of benefit from from it and it will last for years.

(2) it doesn't help you with anything external to the source file that's not tightly coupled through some additional control mechanism. (Note that you pay a price for control mechanisms.) So, it's a small solution to small problems. Quite nice. But limited without paying an additional price, which may be quite expensive depending on the other goals of your system.

I don't agree with the previous poster's assertion that it's a useless hack. But I understand why one might feel that way. The utility of static type-checking seems to be greatly oversold.

I don't get this argument at all.

If you look at a typical call stack - in any language - going from the frontend all the way to the database, there will be hundreds of lines. Two, maybe three of those will be remote jumps.

Every one of those calls is an opportunity for a contract mismatch. Even if static type checking misses a half-percent of your calls, it's still working to enforce 99.5% of your contracts. The value of that is hard to oversell.

There’s nothing wrong with static type checking, It’s generally good and will prevent certain kinds of problems with out the cost of unit tests. I’m all for static checking generally, not just for types.

My point was to point out the limitations. I might be wrong, but people seem to often ignore these limitations. This oversells the benefits and leads to frustrated people like the one whose comment I originally responded to.

99.5% is not nothing, but not something you can live with in most systems so you are probably going to need something more. Also, you are going to be a lot happier if the vast majority of those hundreds of lines in your cross-system call stack are completely agnostic to the types of your problem domain and you want to be careful to minimize the coupling between components living on different sides of the jumps you mention. You can do that, of course, with static types, but it takes more care and intentional design and you really need to acknowledge the issues before you can get it reasonably right.

Tangentially, but in a similar vein, this is my gripe with TypeScript: It leads you down a path that takes the best part of JavaScript out: namely it's dynamicity, and prototypical object orientation, and leaves you with a hamstrung, not-so-good statically typed OO language. When you view it in that light, it hardly stands up to other options out there, IMO of course.

That's why I was lead to something like ReasonML because it solved the things I was already trying to do with my front end code (keep my renders in React immutable, minimize side effects, use more functional paradigms, have strong typing), but it did it with a much more powerful functional language, with a world class type system in which only Haskell rivals.

TypeScript does not take out dynamic typing or prototypical object orientation. If you can more cleanly communicate something dynamically, do it. `any` is right there. (And I do this sometimes! Of course, code where I do this is invariably where most of my unit testing has to be, but I have the choice, and I'm making it.)

And, not to be pointy, but I always get a real weird vibe about people who come rolling in professing that TypeScript isn't a backhoe when it professes to be a shovel. TypeScript is not about a "world class type system" and that's never been the claim. It's about reducing the ways in which normal developers can shoot themselves in the foot. If ReasonML or whatever equally bloggable thing scratches your itch, sure, but you're never getting the 95th percentile of developers, to say nothing of the 50th, to write it--and that 50th, and that 95th, percentile can benefit from better tools, too.

I see what you’re trying to say here, but Reason (and its compiler BuckleScript) aren’t the kind of puristic language folks might think they are. BuckleScript has one of the most comprehensive way to bind to whatever piece of JS you have: https://bucklescript.github.io/ and you can also just include raw JS code as a last resort, all type checked still, while keeping type soundness (in this case, almost like `any` but not really).

HN’s too short to describe the extent of it; if you’d like some language that shares a similar spirit as TypeScript (with extra features such as native compilation), please do check out Reason and BuckleScript. Our main goals are great JS interop, fast build, robust type system and a familiar syntax for JS users.

I'm familiar with the project, and with both F# and OCaml generally. ;) And I'm not saying it's bad or anything, to be clear. I am saying that it's functionally impenetrable to most programmers. To draw an analogy to something less contentious in this crowd, Elixir, even though I quite like it, is doomed to the same fate.

TypeScript is the good-enoughest thing that doesn't surprise, and I think that's enough to put it over the top. (It helps that, in this case, "good enough" is also "very good".)

Thank you for all the work you do btw -- Reason is awesome and I am having a lot of fun learning. Interop seemed daunting at first but after I tried it, it was a great experience.
> TypeScript does not take out dynamic typing or prototypical object orientation

Hence my wording, leads you down the path not forces you to. More often than not, your TypeScript codebase (at least the part you have written yourself or by your team) will be Classical OO style and fully typed, and your code reviews will consist of copious amounts of "we should add a type here"

> It's disturbing that so few people can see how useless it is. To me, it's extremely obvious.

I think this is probably the wrong kind of hubris.

Well, as a someone who had the same initial feelings with TypeScript when it came "oh it slows me down, these types are just a waste of time", I get where you're coming from but think that you have some other issues than TypeScript with your team and project. First of I'd want to say that most of your complaints about "no runtime type validation" in the context of TypeScript/JavaScript is just how unreliable JS is as a language. You just can never be sure and even with TS you can get all the boilerplate of TS with zero benefits if you resort to using `any` anytime you feel too lazy to add difficult types.

Which leads to my next point, the slowness of writing TS compared to JS. I don't know how you maintain your code bases but from my experience any time a new programmer comes in who doesn't know anything about the code, they'll be from time to time wondering, in their heads or aloud, what the hell is the type of some parameter or why something fails on some weird undefined value bug. There's just so much trial and error with JS whereas with TS I don't have to look up at the broken website to see all the errors hitting me as it's all caught with the compiler. Pretty basic static typing stuff but it all comes down to more time writing code, sure, but less time over time maintaining it. And overall the readability of your code is just night and a day with special emphasis towards young programmers or those unfamiliar with the codebase.

What I think has happened is, that you've been bitten by some weird problem regarding API endpoints and incorrectly assumed your typings would hold with an external data source. That just sounds poor programming (don't you use validation libraries such as Joi?) and frankly, a problem entirely of JS itself that such anomalies would happen. If my API starts returning arrays instead of objects sure as hell I'd be angry too but that's just not how it should work. Not a problem of TypeScript, at all.

About the other problems - haven't faced those myself. Most problems I've had have been with writing the correct types especially when they get complex with generics. Sometimes TSC has been annoying and also exporting types into a single typing file seemed quite laborious last time I tried.

Also I think part of the problem is that TypeScript encourages developers to write interfaces that have complex parameters (e.g. accept instances of specific classes) whereas JavaScript encourages simpler parameters like strings, numbers or plain JSON.

In OOP, passing complex instances to functions across different files is dangerous because the different files may all end up affecting the same instance's internal state and you don't know which file is responsible for what change (and it can lead to conflicting states/bugs). The absence of types in JavaScript tends to discourage this design precisely because it makes it difficult to express those kinds of complex/rigid class relationships.

JS encourages developers to keep each kind of "live" instance in a single file and only return raw data from that file. For me, restricting all instance mutations to one file is the secret to writing good OOP code which has clear separation of concerns.

TypeScript doesn't encourage returning classes. Writing classes just as data handlers is still verbose and clumsy. You can have a team that's writing Java in TypeScript and see that, but that's not conventional at all.

Instead, TypeScript encourages returning structurally typed interfaces. Which are just data.

>It doesn't even ensure type safety because there is no runtime type validation for JSON objects received from the API.

what do you mean by type validation of JSON objects? If you mean, that you can for exammple pass string to object property defined as number, then I must warn you, that this is problem in all languages (eg. most java json serializers, will throw exception). Anyway if you need to ensure all object properties have correct type, you should write validator/transformer (there are already libraries that can do it for you).

Our experiences are totally opposite. I find your opinion bizarre...
So you're saying that on your planet TypeScript has runtime type validation?
On this planet it is possible to do runtime type validation with typescript. It's not an out of the box feature but a number of libraries allow it, e.g. https://github.com/gcanti/io-ts
Yeah, you're right, and since vanilla javascript does have runtime type validation that's a reason not to use Typescript. /s

The documentation, tests, breaking run attempts when developing, and flipping back and forth between different source files it replaces, almost painlessly, are 100% worth it.

And you can trivially "nope" out of it anywhere you want if it's getting in your way too much, and congrats, you're back to lovely, pain-free, JS. /s

I'm saying our experiences are totally opposite and that I find your opinion bizarre.
ActionScript 3 was one of my first programming languages and I used it for many years; it's also based on the ECMAScript standard and very similar to TypeScript so I should be biased to like TypeScript, but I really don't.

I was a big fan of static types for many years (I also did Java and C++) so I understand your point of view perfectly but I also understand that it is incorrect. The fact that my POV seems so bizzare suggests that you haven't considered it thoroughly before.

Your view is correct but irresponsible in practice. Validating the schema of all parameters of every method call (because you can never trust the caller in dynamic languages) would be prohibitively expensive. A compiler which injected these checks into every method preamble would have incredible correctness, but would create functionally useless code (if performance is a feature).

> C++

What happens when you call main(argc, argv) with the incorrect argc parameter? There the exact same problem that TypeScript faces at the start of every single C/++ program.

Validate your payloads with JSON schema if you don't trust the server/backend. JSON schema is designed to type-check at runtime and is therefore the correct tool for the job.

Don't curse the hammer if it is unable to loosen bolts.

Stop being dismissive. You know that’s not the point.

I have a few applications that use the same API, and it’s consumed using a shared library in Typescript. There’s a little bit of code that takes raw messages, parses and validates them, then returns typed objects. This means we can use those typed objects confidently throughout the rest of the codebase, making some useful guarantees about the content of them and detecting errors at compile time. Parsing code is isolated and tested in a small area, like you light use unsafe code in Rust.

Yes, it would be nice if this was easier, such that parsing was automatically handled based on those types (and I don’t doubt that there are projects out there that do this). But that obviously doesn’t mean that no value is added.

It’s totally fine if Typescript doesn’t suit your applications, or your way of working - but it might be useful to consider that other people aren’t idiots.

> Stop being dismissive. You know that’s not the point.

Please edit such swipes out of your comments here, regardless of how wrong or annoying you find someone else's comment. Your post would be much better without those bits and the swipe at the end.

https://news.ycombinator.com/newsguidelines.html

All those examples in your last paragraph should be accounted for in data modeling and in the domain logic. Saying a type checking tool somehow prevents this from happening is asinine.
Is this a joke? If your real world data doesn’t have a fixed schema representable in a type system how do you machine parse it?
Check out Rich Hickey on this, eg in the 10 years of Clojure talk (or the transcript).

Static typing requires you to express things in a way that is easily formally provable by the type system. It's much easier to write correct code (=working parer) than the correctness proof in a limited type system. This goes for higher order operations on schemas / types too, eg schemas are easier to make composeable than types.