15 ms·
Type-Safe Printf() in TypeScript
- IceDane 3y agoCool. There is a way to make this easier to extend, though: https://tsplay.dev/WGbEXm https://tsplay.dev/WGbEXm Can't tell off the top of my head if there are any disadvantages to this approach though.
- Touche 3y agoExcept missing the pesky runtime implementation. We don't need though, right? As long as the types say it's right.
- davidmurdoch 3y agoWhat do you mean? `console.log` supports `%d` and `%f` already.
- sroussey 3y agoconsole.log("This is a %s and a %d!", "string", 4.3) -> This is a string and a 4!
- smcl 3y agoI think the point is safely typing the pattern of having variadic functions with a format string argument. The function implementation itself isn’t that interesting, or “pesky” to be honest
- cobbal 3y agoThe static types depicted in typescript are entirely fictitious. Any similarity to runtime types is purely coincidental.
- eyelidlessness 3y agoThis is such a cynical take. The point is to model what types exist at runtime in the type system, so that you can reason about those same runtime types statically. They’re only “fictitious” if they’re defined incorrectly, or if the type system can’t sufficiently express certain of their nuances. The former is usually only the case when developers intentionally work around the safety provided by the type system; the latter is possible, but at this point it’s usually only ever the case for patterns that are hard to reason about regardless of the type system or even the presence of types at all.
- zogrodea 3y agoI find myself inclined to the opinion you're disagreeing with in all honesty. 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.
- quaunaut 3y agoCurious what your issue was with duck-typing. Were you effectively looking to create ADTs that are required to go through a specific step-by-step process, not simply 'look like' the thing that was expected? If so, you might be interested in [newtype-ts]. [newtype-ts]: https://github.com/gcanti/newtype-ts https://github.com/gcanti/newtype-ts
- zogrodea 3y ago
- pkkm 3y agoReminds me of Idris: https://gist.github.com/chrisdone/672efcd784528b7d0b7e17ad9c115292 https://gist.github.com/chrisdone/672efcd784528b7d0b7e17ad9c... 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?
- codr7 3y agoMaybe check out Clojure spec? https://clojure.org/guides/spec https://clojure.org/guides/spec
- yen223 3y agoUnless I'm mistaken, Clojure spec operates at run-time, not at compile-time.
- doctor_phil 3y agoSounds like comptime from Zig. There are a few others that does something similar, but Zig probably has most mind share right now.
- txdv 3y agoYou parse the string and then iterate over the passed arguments and check if everything adds ups. Rather straightforward. Expressing it in the type system like TS did is impressive, but not simple.
- winwang 3y agoI wonder if we should have a kind of "hidden type system", where we still take advantage of having a single type system to reason about, but the extra-specific "weird-ish" types can be hidden, almost like private variables, where visibility is literally hidden from the programmer unless obtained from debug modes or errors.
- ruined 3y agonot sure i understand the utility of this when format strings and string template types already exist. 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-expressions/ https://typescript-eslint.io/rules/restrict-template-express...
- klodolph 3y agoI think this is less about the utility and more about showing off unusual ways to use the TypeScript type system.
- 38 3y ago[flagged]
- dragonwriter 3y ago> If JavaScript was a failure By almost any reasonable measure it is one of the strongest candidates for “biggest success ever as a programming language”. Sure, you can argue that the reasons for that aren’t mainly language design related, but it is absolutely not anything like a failure.
- 38 3y agoPretty easy to succeed when it's the only language a browser supports.
- dragonwriter 3y ago“JavaScript’s success is unfair”, sure, whatever. But that’s not the same as “JavaScript is a failure”.
- baq 3y agoThe point is that success sure tastes bitter.
- dragonwriter 3y agoYes, as expected of a successful language, JavaScript clearly falls into the first category of Stroustroup’s observation: “There are only two kinds of languages: the ones people complain about and the ones nobody uses.”
- amichal 3y agoIt wasn't the only language browsers (vbscript was allowed client side in early ie) supported and also isn't anymore with wasm
- k__ 3y agoNice! Now do ReScript. :D
- taeric 3y agoI've been kind of curious why tricks like this aren't used more to make sql and such. Heck, you could do similar tricks for shell execution. Or any general "string that is parseable." Seems we always take the route of not parsing the string as much as we can?
- aethros 3y ago> why tricks like this aren't used more 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.
- taeric 3y agoApologies, I meant the equivalent of `os.popen` in python. You'd almost certainly only support a subset of what a shell actually supports, but that would almost certainly be for the best. 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.
- lolinder 3y agoThis is a pretty neat application, but most embedded languages like SQL have a way more complicated grammar that would require a really complicated set of types to parse. This can tank the performance of your type checking step and it also means that the error messages you get out of the parser-in-types are going to be nearly useless. 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 https://egghead.io/blog/using-branded-types-in-typescript
- eyelidlessness 3y agoMinor nit: I’ve found types like these—that is, iterative recursive types—benefit from using terminology common to map/reduce. And by “benefit from”, I mean become more understandable by a wider audience—not necessarily the HN audience per se, but quite likely teammates and future selves. 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.
- AbuAssar 3y agothis sounds similar to prolog!
- jitl 3y agoWord of warning: the typescript compiler is not a particularly fast evaluator of recursive list manipulation programs, which is what these kinds of types are. 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.
- quaunaut 3y agoWhile I certainly agree, I've found that this is often an indication of too-complex an architecture, and a fundamental re-think being necessary. I've had projects that depend on [fp-ts], which end up incredibly generic-heavy, but still make it entirely through a typecheck(not build- typescript's just worse at that than other tools like esbuild) in seconds-at-worse. Obviously depends on your organization/project/application, but I do like these things as complexity-smells. [fp-ts]: https://gcanti.github.io/fp-ts/ https://gcanti.github.io/fp-ts/
- jitl 3y agoHow large in lines of typescript are the projects you've used fp-ts or similar with? 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/ 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.
- quaunaut 3y agoNothing that large admittedly- but I have gotten near the 1 million mark(prolly ~800k?) in one project. But I'd also say that at that size(honestly these days, I reach for it pretty much by default) I'd go toward a monorepo that only runs CI on the packages that have changes, as that much JS to even just go through a typical eslint is gonna be a real chore. As a result, the 'complex types' don't end up impacting as much. 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).
- akira2501 3y agoAm I missing something? This is just a toy implementation of a function prototype, that only includes integers and strings?
- aroman 3y agoAs a general rule, if something is on the HN homepage and you find yourself asking "am I missing something?", the answer is almost by definition "yes" :) 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!)
- akira2501 3y agoI really enjoy how people try to dispel their outright attempts at bullying behavior with an emoticon. :) 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.
- pests 3y agoI think it was an attempt to add a cheeky or comical tone to the response, instead of outright saying "Yes, you're missing something" or the more curt "Yes". But if I helps, Yes, you're missing something.
- beders 3y agoHonestly, if you spend that much code on a single `printf`, I will reject your PR and we will have a conversation about code maintenance and cost. Please don't adopt this.
- jitl 3y agoprintf is about 700 lines in musl libc https://git.musl-libc.org/cgit/musl/tree/src/stdio/vfprintf.c https://git.musl-libc.org/cgit/musl/tree/src/stdio/vfprintf.... and there's no language-level type safety, although plenty of tools lint printf now
- crgwbr 3y agoNeat, but this is basically a ripoff of this post from a few years ago (even to the point of not including the runtime implementation): https://www.hacklewayne.com/a-truly-strongly-typed-printf-in-typescript https://www.hacklewayne.com/a-truly-strongly-typed-printf-in...
- yen223 3y agoThe interesting thing here is that the typesafe printf has its function arguments inferred from a string literal, at compile time. You can change the 9 to a "9" and see the type error even before running the code. 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.)