Hyper-Typing
In this article, I talk about an inherent trade-off in TypeScript's type system: stricter types are safer, but often more complex. I describe a phenomenon I call "hyper-typing", where libraries - in pursuit of perfect type safety - end up with overly complex types that are hard-to-understand, produce cryptic errors, and paradoxically even lead to unsafe workarounds.
I argue that simpler types, or even type generation, often lead to a more practical and enjoyable developer experience despite being less "perfect".
TypeScript’s type system is gradual: when describing a JavaScript value with
TypeScript, you can be more or less accurate, ranging from saying that the value
could be anything (any
), to describing in absolute detail what the value is
under different conditions.
Consider for example this function which prints the property of an object - if it exists:
function printProperty(obj, key) {
if (typeof obj === "object" && obj !== null && Object.hasOwn(obj, key)) {
console.log(obj[key]);
}
}
We can type it in a loose way as follows:
function printProperty(obj: any, key: string) {
if (typeof obj === "object" && obj !== null && Object.hasOwn(obj, key)) {
console.log(obj[key]);
}
}
But we can also be more strict, requiring obj
to be an object and key
to
be one of its properties:
function printProperty<Obj extends object>(obj: Obj, key: keyof Obj) {
console.log(obj[key]);
}
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
:
// Passing in a non-existing property gives an error.
printProperty({ a: "a" }, "b");
Having this additional guarantee is obviously desirable, but it comes at the expense of making the type definition more complex. Not much in this case - the type is still very understandable - but it reveals an inherent trade-off. Where should we draw the line?
Hyper-Typing
Lately I’ve been trying out a few libraries that - in pursuit of perfect type safety - make their typings so complex that it makes them almost unusable, in my opinion. I call this approach hyper-typing, and I worry it’s becoming a trend in the TypeScript ecosystem.
I get why, actually. I myself am often a hyper-typer! It’s a slippery slope: “If I add this type constraint then the caller will get an error in this particular case”. “I can make this function infer this type here so the caller will get the correct type hint there”.
At the bottom of the slope you get to a place where yes, things work, and they might also look good for the caller - in the happy case. The resulting types, however, are a complex mess, and the compilation errors produced when the caller deviates from the happy path are walls of inscrutable text.
An Example: TanStack Form
TanStack Form is the new kid on the block of form libraries. It pushes heavily on type-safety, promising “first-class TypeScript support with outstanding autocompletion” for a “smoother development experience”.
What the library accomplishes is honestly impressive. Just give it the default values of your form and you’re set: now for every form field you define - no matter how deeply nested - you get the correct type when you read or write its value, when you do validation, etc.
Don’t look at how the sausage is made, though: you won’t even understand it.
Take
the simple example from TanStack Form’s documentation.
Use the interactive sandbox and try to check what’s the shape of a field’s
meta
property (or any other library value, really). Here’s what you’ll see:

The FieldMeta
type has 17 (!) generic parameters and is the intersection of
two types - each taking the same 17 generics - which is where you eventually
find its properties defined.
To be fair, after re-formatting the type definition file and staring at it for a
couple of minutes, I do start to understand what’s going on. For the FieldMeta
type in particular there’s nothing too obscure, but I can’t help but feel that
this undoubtedly clever and accurate type definition is not actually helping
me as a user of the library.
Cons of Hyper-Typing
TanStack Form is one example of a hyper-typing library, but as I said, lately I’ve encountered others that follow a similar approach and leave me with similar issues:
-
Badly-formatted type definition files. This is technically TypeScript’s fault, but I guess the issue is not apparent until the typings become very complex. It should be easy to fix, though, just by running the files through prettier.
-
Difficult to understand types. I agree that they’re safer and more accurate, but that’s not very useful if I don’t understand what they’re actually describing.
-
Unsafe workarounds. Getting into situations where I need to explicitly define a type is inevitable, and 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. -
Incomprehensible error messages. TypeScript error messages are not amazing to start, and the more complex the type, the more complex the error message.
A Happy Medium
Having banged my head against hyper-typed stuff, I can now say I prefer less accurate, less safe libraries. They might be dumber; I might need to explicitly define a type that technically could have been inferred. But the practical reality is that I find working with them being overall more enjoyable, and the resulting code more understandable and maintainable.
An alternative approach that I also find more enjoyable is having a separate build step that generates types - from a schema definition, for example. In some corners of the internet I’ve seen it being depicted as the ultimate DX sin, but on more than one occasion I’ve actually found it works very well. For example, the way the Astro framework for building static websites generates types for your content collections is just delightful. I really hoped more tools follow in its footsteps.