They’re great in small doses where you really need them, but overuse or widespread use of complex types will make your build slower. It’s much better to avoid generics or mapped types if you can. The typings for a tagged template literal (without digit format specifiers like %d4) don’t require any generics.
I love to write code like this, but I’m guilty of over using fancy types and I flinch when I see a typescript build profile showing 45s+ spent on generic types I wrote without realizing the cost.
Obviously depends on your organization/project/application, but I do like these things as complexity-smells.
[fp-ts]: https://gcanti.github.io/fp-ts/
We have about 3 million; when I discuss a slow type, i mean a type that contributes ~1 min of checking or more across all the uses in 3 million lines, analyzed from a build profile using Perfetto. I've looked at a generic-heavy library that's similar (?) to fp-ts, effect-ts (https://effect.website/), but I worry that the overhead - both at compile time with the complex types, and at runtime with the highly abstracted control flow that v8 doesn't seem to like - would be a large net negative for our codebase.
As to runtime: While v8 doesn't like it, what it doesn't like even more is having code to run in the first place- and I've found that my FP-heavy projects often have fewer lines of code by factors of 3 at worse, often as high as 15. So in general I didn't get much in the way of perf issues, and when there would be a place that perf mattered, I'd then rewrite it to not use the FP stuff and instead be written to purpose.
Basically, I use FP(and by extension fp-ts) as a good default(as it increased velocity by enormous factors, and more as time went on), then reach for the toolbox when the situation called for it.
BIG ASTERISK however: I don't use `fp-ts` much in React however. With it primarily depending on `Object.is` for comparison, the pure nature of the libraries creates a need for a lot of tools I wasn't able to find a satisfactory answer to. So most code like this was either accomplishing things outside of components(components would often call them though), or was backend-focused(ie, Node.js).
Which is to say, these names almost always make types like this more clear:
- Head: the first item in the input type you’re iterating through
- Tail: the remaining items or unprocessed structure you’ll likely recurse on next
- Acc (or pick your favorite “reduced” idiom): a named type for the intermediate product which will become the final type when you finish iterating. This can be provided as an optional parameter with an empty tuple as its default, largely modeling a typical reduce (apart from inverting the common parameter order).
It also helps, IME, to put a “base case” first in the type’s conditions.
When all of these names and patterns are utilized, the resulting type tends to look quite a lot like an equivalent runtime function you could encounter for producing the value equivalent to its type. This is great because you can even write the runtime function to match the type’s logic. This demonstrates both what the type is doing for people who find these “complex types” intimidating, and that the type accurately describes the value it’s associated with.
There is a way to make this easier to extend, though: https://tsplay.dev/WGbEXm
Can't tell off the top of my head if there are any disadvantages to this approach though.
https://www.hacklewayne.com/a-truly-strongly-typed-printf-in...
Recently though, I've been wondering whether advanced type system stuff is the right approach. It usually becomes pretty complicated, like another language on top of the regular language. Maybe it would be easier to have some kind of framework for compiler plugins that do extra checks. Something that would make it easy to check format strings or enforce rules on custom attributes, like Linux's sparse does, using plain imperative code that's readable to the average dev. Large projects would have an extra directory for compile time checks in addition to the tests directory they have now.
But I haven't seen any language community do something like that. What am I missing?
Go has adopted a similar approach to this - they've made it fairly easy to write separate plugins that check stuff like this. The plugins aren't executed as part of the compiler though, they're standalone tools. For example, see golangci-lint, which bundles together a load of plugins of this kind.
Some of these plugins are shipped within the go command directly, as part of the "go vet" subcommand. (including a printf format check, which is similar to what's described in this post, i.e. it checks that arguments are of the correct type).
Expressing it in the type system like TS did is impressive, but not simple.
With the property of verifiably correct behavior
> compiler plugin
A number of languages allow it (Haskell being the most prolific example, but also Java, Scala, gcc, many others)
This is something that most mainstream language's type system cannot do.
(This may be obvious, but a lot of commenters here might have missed that.)
you can also use typescript-eslint/restrict-template-expressions if you find yourself running into problems with that
https://typescript-eslint.io/rules/restrict-template-express...
A more common solution is to parse the string at runtime with a proper parser with decent error handling and then have the parser return a branded type [0] which you can use elsewhere to ensure your strings are well formed.
[0] https://egghead.io/blog/using-branded-types-in-typescript
I'm also curious on the idea of having a string parsed at runtime and why that is necessarily better? Sounds like this is essentially dynamic typing? Where they are calling it branded, instead of dynamic? At first, I confess the idea sounded close to a tagged union. You have to have something in the data to indicate the tag; but I guess it is missing the union part? Definitely looks close to the idea of treating "objects" as maps.
Neat idea, thanks for sharing!
And on the off-chance they get it right, then damn that's pretty great.
There are a bunch of more practical takes that codegen types from your database and generate types for your queries, eg: https://github.com/adelsz/pgtyped
To me the second approach seems much more pragmatic because you don’t need to run a SQL parser in your typechecker interpreter on every build
Your implication that it is a tradeoff, btw, I fully endorse/agree with. And I know there will be pathological projects out there where the code gen will take ages to complete. I'd hazard a guess that most people wouldn't notice the codegen happening on every build for most projects. Especially on modern build machines.
Some languages don't support this.
The languages that do would require extensive systems to implement this feature. It may simply not be a priority over other requirements like thread safety, atomicity, etc.
> similar tricks for shell execution
Shell only supports strings, integers and lists. The type system is too limited for this level of type-checking.
This works in typescript due to the advanced type operations built into the language.
Basic point being that the equivalent of named/delimited parameters with pretty much forced support for escaping such that you have to go out of your way to send raw strings.
I think, bottom line, it bemuses me that the default "convenience" methods are almost always "send this string over to another process to evaluate it" instead of any processing locally on it. That feels it would be far better as the power "escape hatch" instead of the "convenience method" that it is often pitched as.
It's just a cool use of some of typescript's more advanced features that many developers probably don't use on a day-to-day basis (likely for good reason, as other comments have pointed out!)
Meanwhile, the HN homepage is not some carefully guarded display of exceptional merit, and no serious "hacker" would take the things posted here to be above reproach.
Yes, you're missing something.
Now do ReScript. :D
Please don't adopt this.
and there's no language-level type safety, although plenty of tools lint printf now
The function implementation itself isn’t that interesting, or “pesky” to be honest
<rant>The invention, support and defense of Typescript baffles me. It feels like an intensely wasteful work-around for poorly written interpreter error messages concocted by comp-sci grads who think compiled languages are superior to interpreted ones in all situations and they want to bring this wisdom to developers of loosely typed languages. </rant>
The reason you wouldn't want a cross-compiler for that other language is that there are true semantic differences between languages, and many languages simply cannot fully support the JS runtime- and if your primary application is still in Typescript, trying to cross-compile for a single feature is downright ridiculous.
> The invention, support and defense of Typescript baffles me. It feels like an intensely wasteful work-around for poorly written interpreter error messages concocted by comp-sci grads who think compiled languages are superior to interpreted ones in all situations and they want to bring this wisdom to developers of loosely typed languages.
I sincerely don't think statically typed languages are superior, but I'd argue a large part of the increase in quality over the last few years of the ecosystem is due in large part to Typescript.
Is it going to fix all, or even a majority of the issues? Probably not. But if it can improve upon the situation I don't see why we'd make perfect the enemy of good.
Coming from a mostly C/C++ background, I had been very skeptical of “gradually typed” languages like Dart (and now Python), but I’ve come around to the view that for many purposes it’s better than a completely statically typed compiled language.
You don’t need to compile TS at all, just bundle it, which is lightning fast in current tools, so it feels almost as nimble as pure JS. But you have almost-instant type checking in tooltips and code completions, and you can run a full project check whenever you want (that’s slow, but still way faster than compiling Rust).
The type system isn’t perfect but it’s incredibly expressive. Almost anything you could imagine doing in straightforward JS can be typed (admittedly sometimes with a lot of effort and head-scratching) and once typed it’s generally easy to use with confidence that most runtime errors will be avoided.
The fact that JS and TS are separate languages is a bonus -- it keeps TS honest, and the competition between JS engines means runtime performance is great. If you were designing TS from scratch, I think you’d be tempted to add some kind of runtime type reification, but TS is better off without that. Completely erasing types means compilation will always be fast, and in my experience RTTI causes more architectural problems than it solves.
When defining types in other languages, the task is prescriptive (you specify what fields there are in a type and the runtime accepts this as law), but in Typesxript the task is meant to be descriptive (as you say, one models the types that exist at runtime which is the inverse).
I was excited about Typescript when I learned it, but found myself disillusioned by actual experience when using it (of course others love it and have good reason to). Had defined classes in Typescript so I can have some of my types reflected at runtime.