DEV Community

Discussion on: The Trouble with TypeScript

hoichi profile image
Sergey Samokhov

I know this is an old article, but then again, it must have aged well enough for you to include it in your 2-year digest, so here are my 2 cents:

Cent 1: Libs vs Apps

I might have misread, but the experience you describe seems to be mostly as a lib author. Libs, IMHO, are different from apps in that any library worth its salt usually has much more users than it has developers, most of the bugs are very likely to be reported, and maybe even have a PR submitted for. "Given enough eyeballs, all bugs are shallow". Plus you usually have a test suite in place, so adding more tests is usually a low-friction endeavor.

Apps, on the other side, have a lot of code that isn't reused at all, and might even be called very rarery, so it can have a lot of edge cases that might break unknowingly to anybody. Also, people reporting bugs are either QA engineers, or users, so on average, bug reports have less technical details. Sure, tests might help, but I've worked in 4 shops so far, and 3 of them had very little to no tests for the client code (the fourth one has some, but not that many either). All 4 used statically typed languages though (TypeScript or ReasonML/ReScript). Whatever you can say about noisiness of TS annotations, tests seem to be more work to maintain.

And one thing apps have in spades is business logic. Types are good for domain modelling, making illegal states irrepresentable, etc. Do libs need this kind of help with modelling? I don't have a lot of experience with libs (the only lib I've published is very type-oriented: it mostly consists of API), but I have a suspicion that really good libs (or at least FE frameworks) mostly try to create simple abstractions for a lot of technical complexity. And for that kind of problems, static types in implementation can be more hindrance than help, as you have obviously noticed.

Also, I know I don't have enough experience with tests, but. One highly touted benefit of static types is ease of refactoring, and I'm not sure how the same would work with tests. You change a function signature in TS, and the type checker screams at you until you fix all the call sites. Nice and easy. (You'd have to be more careful in TS than in a language with no nullable types, but that's a minor detail.) How to get that kind of safety with tests? I bet you need a lot of coverage (probably with integration/end-to-end tests) to approach this kind of safety net. And sure, I know types won't catch everything that tests will, but again, types are cheaper.

Where I'm going with this? My point is that when authoring a lib, you probably give more thought to design upfront. And you don't refactor as willingly. So ease of refactoring, on the cheap or otherwise, isn't in high demand. And when do refactor, you have a lot of test coverage anyway, so tests are a sunk cost. With apps, the process is usually much more agile, and both ease of refactoring and maintenance cost are very important, so it's no wonder types are used everywhere, but tests are not.

Cent 2: Interface vs Implementation

I know some people think you're not man person enough when you bail out of the type system, but I think it strongly depends on whether it's interface or implementation, and I'd argue that for the latter, type safety is not as useful and doesn't matter as much. The more useful approach could be to write sane types for the interfaces, maybe write tests to make sure it works as expected—and then use whatever number of as any and @ts-ignore as you need to make it work. (In a language like ReScript, which is not a "gradually typed language" like TS, you're still free to combine static types and raw JS like this: let add: (int, int) => int = %raw(`function (a, b) { return a + b }`).)

But when it comes to interfaces, I'm with Jared Forsyth who said: "If it’s hard to type check, it’s probably hard to understand." Communicating the intent of your abstractions is probably as important as solving the problems your abstractions abstract away. So if some howto is hard to express with types, maybe it's to hard to explain at all? Sure, there are exceptions: e.g., defining targets in Stimulus is easy to grok while impossible to type check, but I'm not sure it's something to aspire to.

Conversely, if someone has to use hacks while consuming some statically typed APIs, then either the APIs are bad (or at least a bad fit for the world of static types), or your own types are convoluted, or, on a rare occasion, it's a bad case of impedance match between the API and the problem at hand. Maybe those rare occasions happen often enough with libs where you have to optimize a lot, but again, that's not much of a problem with your typical business logic code.

ryansolid profile image
Ryan Carniato Author

I do agree with the overriding App vs Lib thing. In the article I was dealing this on 3 fronts, as a library author, then as Development Manager on frontend and backend of a Preact/GraphQL project. I will say the backend experience with TS was great. It felt easy and just worked. There were some poor types at first but it was never an obstacle.

On the client-side, it was much more painful. Part of it was just library mismatches and trying to pull of Preact Compat cleanly. And weirdness around libraries designed without TS in mind. All that being said it is a lot easier to sort of just make it work as necessary and take the value where you can. We stayed on TS for the project and the developers seemed relatively happy with it except when they got stuck.

I think I found it much more annoying in a prototyping sort of sense. Sometimes developers would be stuck, and I found it easiest to lay out the logic in simple code. With CoffeeScript my psuedo code would basically execute. We were past that with JS anyway but TS felt like the opposite end of the spectrum. In one sense it's probably better anyway. The old 10x developer leaving behind 10x the number of bugs. But my former development manager was like that and I had continued the tradition. In my career I've never seen any teams deliver that fast before or again. I'm not going to say it isn't without tradeoffs, but I find it interesting when looking at it as a matter of points along a scale. Not simply better/worse.

As a lib author, most of my friction came around functional programming APIs with paths and partial applications, and JSX. Solid uses a custom JSX compilation and typescript definitely has tied my hands more than a few times. I hit about 5 issues within my first week that have been taking years to resolve. They have addressed 2 out of the 5 in the last 2 years. Honestly I think those factors made things particularly awkward. I wouldn't expect most libraries to even hit these sort of things.