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

(tangential) In theory I like TS. But in practice, unless I'm the one writing it and can KISS, it can quickly turn into an unmaintainable nightmare that nobody understands. TS astronauts, left unchecked, can make extremely complex types using generics, conditionals, and all the esoteric features of TS, resulting in extremely obtuse code.

For example, I doubt anyone could explain this "type" without studying it for several hours:

https://github.com/openapi-ts/openapi-typescript/blob/main/p...

In this case, the "type" is really an entire program.



I must be a part of the problem because reading that type isn't too difficult.

I also think types like this aren't innately problematic when they live in libraries. They should be highly focused and constrained, and they should also be tried, tested, and verified to not get in the way, but they can absolutely be a huge benefit to what we do.

Maybe it's mostly problematic when type astronauts litter the application layer with types which are awful abstractions of business logic, because types are far less intuitive as programs than regular JavaScript or common data structures can be. Just type those in the dumbest way possible rather than wrap the definition of them up into monolithic, unnavigable, nested types and interfaces.

If a library allows me to define valid states related to events which drive a data store or something narrow like this, that's awesome (assuming it's intuitive and out of the way). I like this kind of type-fu. If it's trying to force coworkers to adhere to business logic in unintuitive ways, in a domain that's not unlikely to shift under our feet, that's a huge problem.


> I must be a part of the problem because reading that type isn't too difficult. I also think types like this aren't innately problematic when they live in libraries.

Despite the star count on the repo (which, if you aren't paying attention to the 0.X versioning, might lead you to believe it's a well tested "library" type), that particular type I linked to has a ton of bugs with it that are currently documented in at least half a dozen open issues, some of which are super esoteric to solve:

https://github.com/openapi-ts/openapi-typescript/issues/1778...

In this case ^ the problem was due to "behavioral differences based on the strictNullChecks ... compiler option. When that option is false (the default), undefined is considered to be a subtype of every other type except never"

Maybe I'm old school, but as long as we are using metaprogramming to solve a problem, I'd rather codegen a bunch of dumb types vs. implement a single ultra complex type that achieves the same thing. Dumb types are easy to debug and you won't run into strange language corner cases like when `undefined extends` has different behavior when strict mode is on or off.

I guess my point is, maybe you find it easy to read, but apparently it's a nightmare to maintain/test otherwise there wouldn't be so many bugs with it:

- https://github.com/openapi-ts/openapi-typescript/issues/1769

- https://github.com/openapi-ts/openapi-typescript/issues/1525

I'm pretty sure I could fairly easily implement `openapi-fetch` by code generating dumb types and it would avoid all of these bugs, and maybe I should as a reference implementation just for comparison purposes in the future for discussions like this.


I'm not trying to say all types in libraries are okay. There are tons of awful ones there, too. One of my favourite libraries actually has some of the worst typing issues I've encountered, and like you're saying, code generation is the perfect solution for the problems they're facing. They actually had a code generator for a previous version of the library, but significant API changes in the latest version caused the code generator to break.

It's imperative that the crazy astro types actually are good; otherwise they really are just going to get in the way. I think my point about libraries though is that if they're hyper-focused on solving a single problem, there's a better chance that the typing will stay relevant, stable, and improve over time. In an application this seems to be less true, leading to all kinds of clever and/or verbose type definitions trying to solve this and one million other problems at once. It's brutal.

After looking closer at that type you linked to, there's this one embedded type called `MaybeOptionalInit`, haha. MaybeOptional. I guess it's optional, sure, and maybe it won't be provided at all (hence the `never` condition), but... Why is that MaybeOptional and not just Optional? That is a bit weird. I see what's happening but I'm not crazy about how it's implemented.


> I'm pretty sure I could fairly easily implement `openapi-fetch` by code generating dumb types and it would avoid all of these bugs, and maybe I should as a reference implementation just for comparison purposes in the future for discussions like this.

FFR: I ended up doing just that: https://github.com/RPGillespie6/typed-fetch


> TS astronauts, left unchecked, can make extremely complex types using generics, conditionals, and all the esoteric features of TS, resulting in extremely obtuse code.

Disclaimer: I guess I'm a fellow TS astronaut.

Most of the time TS astronauts will stick to your methodology of keeping things simple. Everyone likes simple, I think.

However, the type-austronautics is necessary once you need to type some JS with some really strange calling conventions/contracts (think complex config objects inputs, or heterogenous outputs that end up with _not quite the same_ call signatures, using large objects trees for control flow, etc; basically really shit code that arises from JS being a very dynamic language) without modifying the interfaces themselves. Sure you can be a bit lenient, but that makes the code unsound and crates a false sense of security until the inevitable runtime bug happens.

The correct solution would be to refactor the code, but that's not always possible. Especially if your manager was the author of said magnum anus—apologies, I meant magnum opus—and sabotages any attempts at refactoring.

I guess the moral hiding in this anecdote is that I should looking for a new job.


I will agree that some TS libraries have insanely complicated types, and compared to other programming languages I have used (e.g. Clojure), it takes a longer time to understand library code.

But the example provided here doesn't seem too bad. Here is my attempt after skimming it twice.

  Paths extends Record<string, Record<HttpMethod, {}>>
I assume the

  Record<HttpMethod, {}>
is a hacky (but valid) way to have a map where the keys must be HttpMethod and the values contain arbitrary map-like data. e.g. maybe it describes path parameters or some other specific data for a route.

Moving on.

  Method extends HttpMethod
  Media extends MediaType
These seem self-explanatory. Moving on.

  <Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>>(
    url: Path,
    ...init: InitParam<Init>
  ) => Promise<FetchResponse<Paths[Path][Method], Init, Media>>
Looks like we have two generic parameters: Path should be a type satisfying PathsWithMethod<Paths, Method>. That's probably just requiring a choice of path and associated HTTP method. As for Init, that looks like it's to extract certain route-specific data, probably for passing options or some payload to fetch.

Lastly,

  Promise<FetchResponse<Paths[Path][Method], Init, Media>>
Taken everything I have just guessed, this represents an async HTTP response after fetching a known valid path -- with a known valid method for that path -- together with Init parameters passed to fetch and possibly Media uploaded as multi-part data.

I probably got some details wrong, but this is what I surmised in about 15 seconds of reading the type definition.


Not defending that code - and I agree with you that wild TS code gets nightmarish (I usually call it a “type explosion”) but

Waaaay back when in my C++ days, starting to get into template metaprogramming, the “aha!” moment that made it all much easier was that the type definition could be thought of as a function, with types as input parameters and types as output parameters

Recentlyish, this same perspective really helped with some TS typing problems I ran into (around something like middleware wrapping Axios calls).

It’s definitely a “sharp knife” if you overuse it, you screw yourself, but when you use it carefully and in the right places it’s a super power.


I'd be interested in reading that Axios-wrapper if it's openly available.


It isn’t :/ I’d be down to recreate it if you can point me at an open-source project to do it in! :)

Basically - we had some custom framework-ish code to do things like general error handling, reference/relationship/ORM stuff, and turning things into a React hook.

I rewrote what we had to use function passing, so that you could define your API endpoint as the simple Axios call (making it much easier to pass in options, like caching config, on a per-endpoint basis).

So you’d define your resource nice and simple, then under the hood it’d wrap in the middleware, and you’d get back a function to make the request (or a React hooks doohickey, if you wanted).

But typescript doesn’t really play nice with function currying, so it took some doing to wrap my head around enough of the type system to allow the template type to itself be a template-typed function. That nut cracked when I remembered that experience with C++ typing; in the end it actually came out pretty clean, although I definitely got Clever(TM) in some of the guts.


Back when I used to work in plain js I saw very complicated structures, but there is no type annotation. It’s worse. The horrible part is where the type changes in different cases, so you have to trace everything to know if what you are changing is safe.


You don't have to understand it necessarily to use it. I'm sure there's plenty of library level code that people don't understand. But that's the point. Typescript will tell you if you screw up. A lot of this has to do with generics, and if you're using it and typescript can infer the generic types to use, it'll be a lot simpler and you'll know exactly what's breaking.

And for libraries like this, you'll unfortunately be limited to Typescript ninjas to maintain, but there's no alternative really. I guess use javascript without types, which doesn't remove the dependencies or complexity just hides it away, and who knows what happens at run time


> And for libraries like this, you'll unfortunately be limited to Typescript ninjas to maintain, but there's no alternative really.

The alternative (in this case , at least) is to generate a dumb fetch interface from the openapi spec. You have to generate the openapi spec types anyway, just take it a step further and generate a dumb fetch interface as well and then you don't need complex generics, you just call dumb typed functions in the generated fetch interface.


Oh this is so funny. That exact type was my introduction to typescript! I came over from Python a few months ago for a solo web project, and I struggled with that type mightily!

In the end it took me a few tries to land on something idiomatic and I actually just ended up using inferred types (which I think would be the recommended way to use it?) after several iterations where I tried to manually wrap/type the client class that type supports. Before I found a functional pattern for inferring the type correctly, I was really questioning the wisdom of using typescript when probably several whole days were spent just trying to understand how to properly type this. But in doing so I learned essentially everything I currently know about TS (which admittedly is still very limited) so I don’t think it was wasted time.


Yes, I find myself in type hell with some regularity. TBH it happens with my own codebase too when libraries I want to use are authored by these type astronauts.


I worked in fully typed Java for 8 years before jumping to fully untyped Ruby.

Having leaned hard on the Java type system for many years, I was terrified of the type anarchy.

But it turned out to not be a problem at al. For me at least, being ambitious with writing tests made not miss types at all. In practice, a good test suite catches pretty much any problems typing handles, and then some!

This is only my experience. I'm not saying everyone should or could work that way, or that I'm better than you etc.


My own experience is that working in typed languages, then going to untyped ones... your sense of types and the problems they address is already fairly high. Your 'untyped' code likely still avoids a lot of the problems you might otherwise encounter, just because of whatever habits you may have picked up in the typed system. Going the other way - untyped to typed - tends to present a lot of ... tough moments along the way, because you're having to put a lot more thought about things that you didn't have to before.


Nitpick: Ruby isn't untyped, it's dynamically typed.

Forth and assembly are untyped, as these languages truly lack distinctions between different kinds of data.


>For example, I doubt anyone could explain this "type" without studying it for several hours

From skimming it for about a minute it seems like it's just a strongly typed way to generate HTTP requests? It really doesn't look too complicated


Well it is. You won't discover how deep the rabbit hole goes with that type until you start trying to debug it. For example, try fixing this issue which is the result of a problem with that type:

https://github.com/openapi-ts/openapi-typescript/issues/1769


Interesting, I wonder how much of that is due to poor implementation by the authors vs. issues with TS vs. issues inherent to building a typed language on top of the mess that is JavaScript?

Most languages with strong type systems (I'm thinking at least as strong as Java or C#, maybe stronger) wouldn't have those same sort of footguns. In C# I've run into other kinds of fun nightmares with types, like trying to use interfaces with Entity Framework Core. But I think that's more EF Core's fault than C#'s.


For your linked example…

It has documentation

> This type helper makes the 2nd function param required if params/requestBody are required; otherwise, optional

The type here is the implementation not the documentation. I guess we are so used to types being the documentation, which they are for value/function level programming, but not in type level programming.

I think maybe you are disappointed at the tooling? I do think the docs here should be attached to the type so that it appears in the IDE as well.


Honestly that type you linked looks like a dream to use (I've never OpenAPI). I love APIs that enforce any necessary restrictions on the type level.


It would be... if it wasn't so bug ridden at the moment


side note: don’t have much experience with TS, but the overuse of extend is also common in “enterprise” Java/C# apps


Eh, not anymore. Arbitrary inheritance chains are frowned upon in C# and people get mad when you do so. You also occasionally run into everything sealed by default because of overzealous analyzer settings or belief that not doing so is a performance trap (it never is). Enterprise does like (ab)using interfaces a lot however.




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

Search: