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.
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?
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.
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`.
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"})
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)
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. :)
I think typescript is just not a very ergonomic static typing system.
Don't get me wrong, it's a fantastic feat of engineering. It's wonderful that it exists. But it's retro-fitted onto a very dynamic language and it shows.
I prefer static typing - but when writing typescript I often question why I'm bothering.
My happy place seems to be typescript, with strict mode, and using //@ts-ignore about every 100 lines or so, usually inside a function.
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”.
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.
This resonates so much with me, like the last 12 years of my life.
I've spent the last ~4 months building a new Rust crate, Typesynth, based on that experience and many of the challenges highlighted in this article.
The general idea is a fully declarative, git embedded and addressable, composable context language where all declarations are decomposed, traced, stacked, merged, and stored in in-memory CAS for immutable access to everything in the composed context. Those contexts can then the "projected" into any form, yaml, json, PyO3, petgraph, etc. as needed.
My inspiration came from working on a Python codebase I initially built almost a decade ago that was based on a layered, hierarchically merged yaml "recipe" for delivery. Tasks in the framework originally had a task_options dictionary. We later built infrastructure for using Pydantic for task_options but never rolled it out to most of the tasks.
I felt the pain of that last year, trying to build UX on top of those tasks and really missing the Pydantic models. So, I went the opposite direction, building a FastAPI app with hundreds of Pydantic/SQLModel models (GitHub API, Salesforce API, Infisical API, etc).
Typesynth is my first ever Rust project. I've put a LOT of time into a proc macro framework to make the whole framework fast with the ambitious goal of composing complex yaml/json in <1ms. Rushing through the final features to be able to release the prototype and share here. Registered the placeholder crate last week!
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.
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.
> 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).
Glad there’s finally a name for this phenomenon. Effect.js particularly egregious in this area. In my own libraries, I’ve definitely found myself producing less-than-perfect type safety because the readability tradeoff was too great.
Overall, I think one day the gradual type system trend will be regarded as a misstep. I’d rather just manually define a new type than play the generics mini-game.
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 :-)
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
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.
Here I was thinking this was about someone typing at superhuman speed. LOL!
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.
> 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.
"Do not put all generics in one basket"
Small discussion, including post from author (19 points, 13 days ago, 13 comments) https://news.ycombinator.com/item?id=43893127
[dead]
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.
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.