[HN Gopher] Type-Safe Printf() in TypeScript
       ___________________________________________________________________
        
       Type-Safe Printf() in TypeScript
        
       Author : wazbug
       Score  : 71 points
       Date   : 2024-03-24 15:02 UTC (7 hours ago)
        
 (HTM) web link (www.typescriptlang.org)
 (TXT) w3m dump (www.typescriptlang.org)
        
       | IceDane wrote:
       | Cool.
       | 
       | 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.
        
       | Touche wrote:
       | Except missing the pesky runtime implementation. We don't need
       | though, right? As long as the types say it's right.
        
         | davidmurdoch wrote:
         | What do you mean? `console.log` supports `%d` and `%f` already.
        
         | smcl wrote:
         | I 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 wrote:
         | The static types depicted in typescript are entirely
         | fictitious. Any similarity to runtime types is purely
         | coincidental.
        
           | eyelidlessness wrote:
           | This 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 wrote:
             | I 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 wrote:
               | Curious 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
        
               | zogrodea wrote:
               | Thanks for the link. That was exactly my use case and I
               | should remember your helpful suggestion next time I use
               | TS.
        
           | gibbitz wrote:
           | Yeah. I see a lot of "typescript is more readable" arguments
           | out there, but I find this code dense and verbose. The more
           | words you use to explain something the more likely you are to
           | be misunderstood. What we're looking at here is basically a
           | restricted wrapper for console.log and a regex implementation
           | meant to simulate a logger in another language. Why not just
           | write a cross-compiler for that language? There's no learning
           | curve for the syntax then, only the target platform.
           | 
           | <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>
        
             | quaunaut wrote:
             | This isn't typical application code, but either proof-of-
             | concept or library code. What you'd instead get is the type
             | error, which in turn would explain itself well enough- that
             | it expected a certain type, and got a different type.
             | 
             | 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.
        
             | iainmerrick wrote:
             | Hmm, let me try to defend TypeScript, then. I think it's a
             | terrific language, and more importantly manages to salvage
             | JavaScript into a very decent language.
             | 
             | 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.
        
               | smackeyacky wrote:
               | > Hmm, let me try to defend TypeScript, then. I think
               | it's a terrific language, and more importantly manages to
               | salvage JavaScript into a very decent language
               | 
               | Counterpoint: It's a desperate attempt to make Javascript
               | useable and nearly does so, but ends up being weird in
               | itself to get around the limitations of the underlying
               | language.
               | 
               | I use Typescript most days and I hate it. Part of that is
               | that npm is a dumpster fire but a lot of it is that
               | Typescript is a rubbish version of much better languages
               | and it hurts to use it. I am so desperate to get back to
               | something sane like C# that I will quit this job for less
               | money.
        
           | tengbretson wrote:
           | Guardrails won't keep you on the road if you intentionally
           | steer into them at full speed either.
        
           | quaunaut wrote:
           | I mean, all types are "entirely fictitious" as far as the
           | computer is concerned. Yeah they usually have fewer layers
           | than JS does, but that's a pretty arbitrary line to draw.
        
       | pkkm wrote:
       | Reminds me of Idris:
       | 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?
        
         | doctor_phil wrote:
         | Sounds like comptime from Zig. There are a few others that does
         | something similar, but Zig probably has most mind share right
         | now.
        
           | txdv wrote:
           | You 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 wrote:
         | I 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.
        
           | SoylentOrange wrote:
           | You mean like the C++ auto keyword but everywhere?
        
         | paulddraper wrote:
         | > another language
         | 
         | 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)
        
         | jamespwilliams wrote:
         | > Maybe it would be easier to have some kind of framework for
         | compiler plugins that do extra checks. [...] 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).
        
         | codr7 wrote:
         | Maybe check out Clojure spec?
         | 
         | https://clojure.org/guides/spec
        
         | keybored wrote:
         | I don't see why static assertions wouldn't be enough in this
         | case.
        
       | ruined wrote:
       | not 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-express...
        
         | klodolph wrote:
         | I think this is less about the utility and more about showing
         | off unusual ways to use the TypeScript type system.
        
       | k__ wrote:
       | Nice!
       | 
       | Now do ReScript. :D
        
       | taeric wrote:
       | I'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 wrote:
         | > 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.
        
         | lolinder wrote:
         | This 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
        
         | quaunaut wrote:
         | There is actually efforts in the Typescript community
         | attempting to do just that. Personally I think it'll end up
         | being a waste, but these sorts of experiments, even when they
         | fail, often can help along new discoveries.
         | 
         | And on the off-chance they get it right, then damn that's
         | pretty great.
        
         | shirogane86x wrote:
         | I think, from having it used recently, that supabase's TS
         | library does this. I had to write a wrapper around it a few
         | months ago at $dayjob and was really surprised when select/from
         | parts of a "query" (not really a SQL query, because it's just a
         | postgrest query) actually got parsed at compile time and spit
         | out the right types. And since our code is pretty type heavy, I
         | was gonna have to do that anyway, so I really appreciated it
        
         | jitl wrote:
         | There is an implementation of SQL that operates on a table
         | shaped type, entirely at type level. For your amusement:
         | https://github.com/codemix/ts-sql
         | 
         | 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
        
         | mind-blight wrote:
         | I can't find it now, but someone actually built that for SQL in
         | Typescript as an experiment. The problem folks run into is IDE
         | and compiler performance. These sorts of features are what make
         | your system turing complete, so they start stressing the
         | compiler pretty quickly
        
       | eyelidlessness wrote:
       | Minor 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 wrote:
         | this sounds similar to prolog!
        
       | jitl wrote:
       | Word 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 wrote:
         | While 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/
        
           | jitl wrote:
           | How 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/),
           | 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.
        
         | kevingadd wrote:
         | The nature of ts also means that if you make your files slower
         | to build via type/list nonsense, the language server is going
         | to bog down and the editing experience will mysteriously become
         | bad for everyone. Strongly discourage doing slow stuff with the
         | type system.
        
       | akira2501 wrote:
       | Am I missing something? This is just a toy implementation of a
       | function prototype, that only includes integers and strings?
        
         | aroman wrote:
         | As 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 wrote:
           | I 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 wrote:
             | I 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 wrote:
       | Honestly, 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 wrote:
         | printf is about 700 lines in musl libc 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 wrote:
       | Neat, 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...
        
       ___________________________________________________________________
       (page generated 2024-03-24 23:01 UTC)