5/18/2025 at 9:49:14 PM
IMO it’s great when libraries are fully typed: it’s like documentation you experience at the moment of use. I think what the author is really dealing with at “when the library types are so difficult to understand and use, I often end up resorting to casting things as any, losing more type safety than I gained” is more the API design being unwieldy rather than the typing itself. You can fully-type a terrible API just as well as a great one and the terrible API will still be a pain to use.by jasonthorsness
5/19/2025 at 3:04:51 AM
I think they’re talking about something slightly different, and they allude to it by saying the useful complex types on the happy path become less useful when something goes wrong.What I believe they’re encountering is that type errors—as in mistaken use of APIs, which are otherwise good when used correctly—become harder to understand with such complex types. This is a frequent challenge with TypeScript mapped and conditional types, and it’s absolutely just as likely with good APIs as bad ones. It’s possible to improve on the error case experience, but that requires being aware of/sensitive to the problem, and then knowing how to apply somewhat unintuitive types to address it.
For instance, take this common form of conditional type:
type CollectionValue<T> = T extends Collection<infer U>
? U
: never;
The never case can cause a lot of confusion, especially at a distance where CollectionValue may be invoked indirectly. It can often be a lot easier to understand why a type error occurs by producing another incompatible type in the same position: type CollectionValue<T> = T extends Collection<infer U>
? U
: 'Expected a Collection';
(I’ve used somewhat simplistic examples, because I’m typing this on my phone. But hopefully the idea is clear enough for this discussion!)
by eyelidlessness
5/19/2025 at 6:01:36 AM
What about the (incredibly unlikely, i'll admit) scenario where somebody attempts to pass the literal 'Expected a Collection' as an instance of this type? What's the best way to insert a warning, but also guarantee the type is unsatisfiable?('Expected a Collection' & never)?
by arjvik
5/19/2025 at 5:14:50 PM
It’s very situational. If you can predict the shape of error cases, anything that doesn’t match that shape will do. If you can’t, you can fabricate a nominal type in one way or another (such as the symbol suggestion made by a sibling commenter, or by a class with a private member). The broad strokes solution though is to use a type that:1. Won’t be assignable to the invalid thing.
2. Conveys some human-meaningful information about what was expected/wrong and what would resolve it.
by eyelidlessness
5/19/2025 at 6:21:34 AM
Perhaps you could make it a private Symbol? Then it should be impossible semantically to use it from the outside.by bubblyworld
5/19/2025 at 3:53:40 PM
Thanks for the tip ill try it outby cdaringe
5/19/2025 at 10:30:58 PM
I haven’t written a complicated library so I’m just guessing, but something as general as a form builder is probably so jam-packed with edge cases and configurability that I’d guess the types need to be that complex in order to be accurate.I think it’s an inevitable trade off: the safer and more specific you want your type inference to be, the more inscrutable your generics become. The more accurately they describe complicated types, the less value they serve as quick-reference documentation.
Which makes me think the types are not the problem, it’s the lack of quick reference documentation. If a complicated type had a little blurb that said “btw here are 5 example ways you can format these args”, you wouldn’t need to understand the types at first glance. You’d just rely on them for safety and autocomplete
by chamomeal
5/18/2025 at 10:05:15 PM
I agree with this.The error messages in TypeScript can be difficult to understand. I often scroll to the very bottom of the error message then read upward line-by-line until I find the exact type mismatch. Even with super complex types, this has never failed me; or at least I can't recall ever being confused by the types in popular libraries like React Hook Form and Tanstack Table.
Another thing I find strange in the article is the following statement.
I often end up resorting to casting things as any [...]
Every TypeScript codebase I have worked with typically includes a linter (Biome or ESLint) where explicit use of `any` is prohibited. Additionally, when reviewing code, I also require the writer to justify their usage of `as` over `satisfies`, since `as` creates soundness holes in the type system.Lastly, I wish the author had written a bit more about type generation as an alternative. For instance, React Router -- when used as a framework -- automatically generates types in order to implement things like type-safe links. In the React Native world, there is a library called "React Navigation" that can also provide type-safe links without needing to spawn a separate process (and file watcher) that generates type declarations. In my personal experience, I highly prefer the approach of React Navigation, because the LSP server won't have temporary hiccups when data is stale (i.e. the time between regeneration and the LSP server's update cycle).
At the end of the day, the complexity of types stems directly from modelling a highly dynamic language. Opting for "simpler" or "dumber" types doesn't remove this complexity; it just shifts errors from compile-time to runtime. The whole reason I use TypeScript is to avoid doing that.
by koito17
5/18/2025 at 10:12:27 PM
Yeah react router is neat in this regard I was confused then pleased to see this the first time I used it import type { Route } from "./+types/home";
Which lets me avoid a lot of manual typedefs
by jasonthorsness