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.
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
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.
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.
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.
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.
(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...)
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 :-)
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”.
[0] https://zod.dev/?id=refine as an example
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.
import type { Route } from "./+types/home";
Which lets me avoid a lot of manual typedefsWhat 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!)Nothing I would recommend, perfect doesn't mean its a good idea.
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
{ 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)
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.
- 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.
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`.
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"})
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).
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?
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.
zig may be like that too, but not tried.
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.
pscanf•1w ago
agos•1w ago
matijs•1w ago
Also curious about the delightful type generation Astro uses.
shirol•1w ago
akdor1154•1w ago
gwking•1w ago
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.
6gvONxR4sf7o•6h ago
p1necone•6h ago
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.