Hyper Typing

62 azhenley 51 5/18/2025, 8:41:51 PM pscanf.com ↗

Comments (51)

freeqaz · 3h ago
Does anybody have a good example of an 'alternative' that handle complex static types more gracefully? The Go language discourages Generics in favor of empty interface which feels similar to what the author is arguing... but I also find myself not always loving Go because of that. (I heavily lean on things like .map() in TS).

Trying to think of alternatives, I can only think of Haskell and C++ which are their own flavors of pain. In both C# and Java I've fallen into Hyper Typing pits (often of my own creation eee).

So what else exists as examples of statically typed languages to pull inspiration from? Elm? Typed Racket? Hax? (Not Scala even though it's neat lol)

Anybody have any tips to explore this domain in more depth? Example libraries that are easy to debug in both the happy and unhappy cases?

xlii · 6m ago
Lately I’ve been exploring zig and outside of painfully underdeveloped documentation I haven’t had so much fun with types (of comptime) for the long time.

I find it much more ergonomic than Rust and less energy draining than OCaml.

Then there’s pattern matching, but IMO Elixir is heading in the wrong direction. Erlang has accumulated dust over the decades. Clojure is very interesting choice because it can do both „comptime” (i.e. macros) and pattern matching.

woah · 2h ago
I've seen Go codebases where serialization was used as a way to generics... ugly.

The generic code would take a string of the serialized struct, which could be passed through (the code was operating on an outer structure) then be deserialized at the other end, preserving the type information. Maybe it could have been handled by some kind of typecasting plus an enum containing the name of the type (don't remember the specifics of Go right now), but the devs had halfway convinced themselves that the serialization served another purpose.

dlahoda · 2h ago
lean4 unifies types and values, so that types feels so natural. you stop thinking types at all as you think about types in rust or csharp. and haskell does not have that feeling too.

zig may be like that too, but not tried.

lblume · 49m ago
Yes. It might sound counter-intuitive or even ironical but the next logical step is to make the type system more, not less expressive and to add completely dependent types. Every language already has them to the extent of Array<T, N> (or T[] & { length: N }, or a recursively defined Tuple<T, N>), but having true flow- and value-dependent types would allow for more concise and expressive code.
jasonthorsness · 7h ago
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.
eyelidlessness · 1h ago
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!)
koito17 · 6h ago
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.

jasonthorsness · 6h ago
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
chris_armstrong · 5h ago
Many of the highlighted problems come from TypeScript allowing such complex type definitions in the first place, which in turn stems from JavaScript allowing such an open and untyped (and yet convenient) compositional model.

The expressiveness of JavaScript is a curse upon every library author who (in vain) may try to design an interface with a simpler subset of types, but is undone by the temptation and pressure to use the flexibility of the language beneath.

The author's instincts are right though - target a simpler subset of TypeScript, combine code generation with simpler helper libraries to ease the understandability of the underlying code, and where a simpler JavaScript idiom beckons, use runtime safety checks and simpler types like `unknown`.

chris_armstrong · 5h ago
Might I add - this isn't limited to languages like TypeScript which simply try and make underlying untyped languages (like JavaScript) safer. It affects well-designed strongly-and-statically-typed-from-the-ground-up languages which have expressive type-constructs (Rust, anything ML-based, etc.) used to reduce repetition and code-generation steps.
foota · 1h ago
This is true to some degree, but something about javascript/TS seem to lead to just incomprehensible typings. Imo any types where you start having to look at the keys of some map is going to lead to pain, but most other languages don't let you do that.
nine_k · 6h ago
The examples of bad, overly complex types are indeed unpleasant and unwieldy: colossal, highly nested types with long, cryptic lists of type parameters.

I think this speaks of lack of abstraction, not excess of it.

If your type has 17 type parameters, you likely did not abstract away some part of it that can be efficiently split out. If your type signature has 9 levels of parameter type nesting, you likely forgot to factor out quite a bit of intermediate types which could have their own descriptive names, useful elsewhere.

js8 · 2h ago
Unfortunately many languages have poor support for named type definitions and higher order types, unlike e.g. Haskell. That would definitely help to avoid these problems.
nine_k · 51m ago
This is so, but Tyepscript has a pretty good support for that.
sberens · 6h ago
I wish IDEs had more features/tooling around types. For example, something like "expand all types by one level" where

{ foo: Bar } would expand to { foo : { bar1: string, bar2: Baz } } (and you could trigger it again to expand Baz)

(this would be especially nice if it worked with vscode/cursor on-hover type definitions)

calderwoodra · 6h ago
The poor support for types in VS Code has always been a blocker for me adopting it vs. Jetbrains, where types and the intellisense are much easier to jump between.
dhorthy · 2h ago
This guy gets it
Waterluvian · 7h ago
I’ve been absolutely loving template types and dot notation pathing. I have an entire compile time (and therefore autocompletable) argument for major.minor.patch.theme.schemaname for all schemas in the program manage. I don’t consider these “hyper typing” because they’re very, very easy to reason about when used in the right context like dot notation paths.

I wish, however, I could cleanly type “this must be an integer between 0 and 58” but typescript isn’t that expressive unless you do some pretty ridonkulous things. Especially with template strings it would be so cool to have something like:

type foo = `v${0:1}.{0:99}.{0:}`

(or whatever pre-existing format exists elsewhere. I just made that up)

This would be generalized as a “number range literal”, maybe. So not particular to template strings.

But not regex. Solving this with a regex literal type would be the poster child of “hyper typing”.

zdragnar · 7h ago
There are libraries that let you define refinement types[0] but it does require a bit of runtime overhead, and the added complexity depends on the library.

[0] https://zod.dev/?id=refine as an example

skrebbel · 7h ago
I love the term "Hyper Typing" and I hope it becomes commonplace. I've long been searching for a phrase with a similar to "premature optimization" (with a similarly mildly-negative connotation) but for overengineered type safety and I think this is it.
dhorthy · 2h ago
Great post and def needs to be said. Won’t name names but there are some VERY popular ts library that are guilty of this. Once you have type constructor constructors I tend to bail and just cast things as described here. But then you lose guardrails, and confidence that you’re consuming the library properly
pscanf · 13d ago
Author here. Curious to hear if anyone's experience also matches mine, or if instead you find the trade-off to be worth it most of the times. :)
6gvONxR4sf7o · 7h ago
I've felt the same, but I blame error messages and language ergonomics rather than the practice itself. Basically, everything you said, but with optimism that future languages and language implementations make 'hyper typing' a good practice. Recent languages have shown that there's a lot of room for improvement on error messages in complex programs. Hopefully that extends to complex types before too long.
shirol · 10d ago
I don't use typing for correctness. I use it for documentation. That's why I prefer JSDoc these days. I only type top-level variables/functions to get hints from my editor. Everything inside a function body remains untyped unless necessary. It’s the benefit of using Typescript without being forced to write dumb code just to satisfy the compiler.
gwking · 10d ago
I started using swift with a lot of enthusiasm for the type system, but at times it was a huge time suck. There were lots of obscure interface types that read like BipartisanPoliticallyCorrectSequence<T> that made writing my own generic utilities challenging. Documentation for that stuff was very poor and the source code was often totally inscrutable due to the implementation of core types in c++ and the overall complexity of the compiler.

I recently saw Chris Lattner talk about Mojo and he made passing reference to Swift trying to do too much with the type system. It’s telling that a guy with his experience is trying something more like zig’s comptime approach to types after two decades of generics.

p1necone · 7h ago
I certainly felt guilty reading it.

Having worked on large codebases with many developers of varying levels of experience I have noticed that bugs that can be written very often will be written - a sort of programming specific version of Murphy's law. So I try to make the ones that seem the most likely impossible. Sometimes I go too far.

agos · 13d ago
my experience absolutely matches yours. Navigating the types of many libraries is often daunting (MUI, React-Aria, react-hook-form to name a few)
akdor1154 · 10d ago
Really nice write-up, thanks. The issues you raise with complex typing are really nicely set out. It's such a trade-off, and you're absolutely write to claim that sometimes, simplicity trumps perfection.
matijs · 10d ago
Absolutely matches your experience!

Also curious about the delightful type generation Astro uses.

CapsAdmin · 5h ago
I find that these complex types often reflect the underlying complex code. This usually happens when you interact with something that is explicitly written for javascript and takes advantage of its dynamic nature. (usually the base javascript api)

window.addEventListener(event, callback), dispatchEvent(event) and removeEventListener(callback) is a good example. In a dynamic language, this api is at least unsurprising. It's easy to understand.

In a typed language, although strategies could vary, one would probably not write the api like that if you prefer to have simpler types.

Something like this would make more sense in a typed language:

import { onChange } from 'events'

const event = onChange.Add((event) => {

})

event.Remove()

// ..

event.onChange.Dispatch({value: "1,2,3"})

codethief · 5h ago
> For example, the way the Astro framework for building static websites generates types for your content collections is just delightful. I really hope more tools follow in its footsteps.

Oh god, I really hope not. Those generated types are an abomination and have caused me so much pain. And don't even get me started on the ridiculous number of bugs I've run into in their type checker (which more or less wraps TSC but does some additional magic to handle their custom .astro file format and others).

codr7 · 6h ago
At the peak of my hyper typing trip, doing lots of Haskell and C++, I was trying to encode all the column/table/query types of my database in the host language.

Nothing I would recommend, perfect doesn't mean its a good idea.

8n4vidtmkvmk · 6h ago
Kysley does a good job of this. I haven't found it annoying.

Well... I think MySQL is a 2nd class citizen so I had to write my own schema gen but that only burned a few hours. Now it's great

riwsky · 3h ago
masto · 1h ago
I jumped in to the TypeScript deep end a few months back. I build a lot of web applications back in the 2000s, then disappeared onto a big tech island where everything was a little different. After popping back out into the real world, I wanted to see how the cool kids are doing things these days, so I figured I would try full immersion by creating a bunch of Next.js sites.

For the purpose of the experiment, I turned every linter and compiler strictness to maximum, and enforced draconian code formatting requirements with pre-commit hooks. Given that my last language love was Perl, I thought I would despise TypeScript for getting in the way. To my surprise, I think I like it. It's not just complexity like I hated in C++ and tedious boilerplate like I hated in Java. The complexity is highly expressive and serves a purpose beyond trying to protect me from a class of bugs that are frankly pretty rare. When done well, TypeScript-native APIs feel a lot more intentional and thought out. When I refactored my code from slinging bags of properties around to take more advantage of TypeScript features, it shook out weaknesses in the design and interfaces.

I've definitely run into those libraries, though, where someone has constructed an elaborate and impenetrable type jungle. If that were an opaque implementation detail, it would be one thing, but I find these are often the libraries where there's little to no documentation, so you're forced to dig into the source code, desperately trying to understand what all of this indirection is trying to accomplish.

The other one that surprises me when it pops up (unfortunately more than once) is the "in your zeal to keep the implementation opaque, you didn't export something I need, so I have to do weird backflips with ReturnType<> and Parameters<>" problem.

Nevertheless, on balance, I'm pretty happy.

coryvirok · 6h ago
Apparently an unpopular opinion, but actually strong types are useful above and beyond editor linting errors.

- Jit optimizations - Less error checking code paths leading to smaller footprints - Smaller footprints leading to smaller vulnerability surface area - less useful: refactorability

Don't get me wrong, I love the flexibility of JavaScript. But you shouldn't rely on it to handle your poorly written code.

arcfour · 7h ago
I can definitely relate to this post. (And don't get me started on those auto-generated SDKs in Typescript, eugh!)

I am far from an expert on type safety or JavaScript, so take this with a large grain of salt, but for anything I write for me, I like my simple JSDoc "typing" for that reason. It feels like any time I introduce TypeScript into anything I'm doing, I now have another problem. Or, more accurately, I spend more time worrying about types than I do writing code that does things that I find useful. And isn't the goal to save time and make development easier? If not, then what's the point?

I should clarify I am not a developer by trade or education and I am mostly doing things more closely related to systems programming/automation/serverless cloud things as opposed to what a lot of other people working with TS might be doing. So my perspective might be a bit warped :-)

BarryGuff · 7h ago
Here I was thinking this was about someone typing at superhuman speed. LOL!
jongjong · 3h ago
I've been trying to get this point across for some time but people tend to ignore incentives when discussing technical matters. These arguments often come across as 'hand-wavy' even though the effects are significant in practice. Incentives and human psychology matters a lot and a type system should be seen as a tradeoff and not a win-win.
ninetyninenine · 4h ago
I disagree. Types are in itself a coding language and as a result they can get arbitrarily complex. When you code your types do it like you code regular code. Use aliases and proper naming. Don’t let a type be that many words long.
omneity · 10d ago
> The strictness even allows us to remove the if check inside the function, since now TypeScript gives us the compile-time guarantee that obj will always have property key.

This is a dangerous suggestion. While the author does acknowledge it is a compile-time guarantee only, that doesn’t imply it is safe to remove the if inside the function.

An API call, reading a file, calling less well-behaved libraries or making some system calls can all result in objects that pass compile-time checks but will cause a runtime exception or unexpected behavior.

As for the thesis of TFA itself, it sounds quite reasonable. In fact a high “level” of typing can give a false sense of security as that doesn’t necessarily translate automatically to more stable applications.

pjc50 · 10d ago
> An API call, reading a file, calling less well-behaved libraries or making some system calls can all result in objects that pass compile-time checks but will cause a runtime exception or unexpected behavior.

Yeah, that is a design flaw that makes this kind of solution less useful than it might be. C# has this problem with "nullable": just because you've marked a type as not nullable doesn't mean some part of the program can't sneak a null in there. Haskell people wouldn't stand for that kind of nonsense.

Klaster_1 · 10d ago
> An API call, reading a file, calling less well-behaved libraries or making some system calls can all result in objects that pass compile-time checks but will cause a runtime exception or unexpected behavior.

These all boil down to implicit `as` type casting parsed boundary data into expected types. What you suggest is replacing casts with to type narrowing guards, libraries like Zod help with some of that. I think TS needs a special flag where `JSON.parse` and alike default to `unknown` and force you to type guard in runtime.

jfjfjtur · 10d ago
I think the point though was that practical solutions can be imperfect, and spending complexity in an attempt at perfection can lead to impractical solutions.
jy14898 · 10d ago
> An API call, reading a file, calling less well-behaved libraries or making some system calls can all result in objects that pass compile-time checks but will cause a runtime exception or unexpected behavior.

Seems crazy to me to have this attitude, the whole point of typescript (and indeed many other languages with type checkers) is that we can leave out unecessary checks if proven by the compiler. The burden of compatibility is on the caller to ensure they supply correct values

omneity · 10d ago
I don't systematically pick one or the other location for these guards, but wouldn't it make more sense to have it in one place, the function itself, both for DRY and to ensure it being checked, rather than on every call site?

Such a requirement "oh yeah always guard your arguments for calls against this function for the "same" thing your compiler is doing anyway" shouldn't be implicit and duplicated everywhere if it's always meant to be fulfilled.

jy14898 · 10d ago
I think I take the attitude that you parse/validate data as soon as possible (ie when reading a file/coming from an API), so that the rest of the code can rely on typechecks alone.

That said, I come from a background where the language doesn't let you consider that the type of a value might be wrong (Haskell, for example), so perhaps I have more trust than typescript deserves.

gnabgib · 8h ago
Small discussion, including post from author (19 points, 13 days ago, 13 comments) https://news.ycombinator.com/item?id=43893127
dang · 7h ago
Thanks! That one didn't get much frontpage attention so maybe we'll merge those comments hither.

(Note the merge process relativizes the timestamps on the comments, so if you see confusing timestamps, that's why (https://hn.algolia.com/?dateRange=all&page=0&prefix=true&que...)

KevinMS · 4h ago
Typescript will eventually get the bad reputation of confusing types just like perl got the reputation of being line noise, then everybody will ridicule it and move on to something else.