[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)