[HN Gopher] An approach to optimizing TypeScript type checking p...
       ___________________________________________________________________
        
       An approach to optimizing TypeScript type checking performance
        
       Author : beerose
       Score  : 91 points
       Date   : 2024-08-12 16:48 UTC (2 days ago)
        
 (HTM) web link (www.edgedb.com)
 (TXT) w3m dump (www.edgedb.com)
        
       | codeflo wrote:
       | These days, TypeScript is effectively nothing more than a high-
       | powered linter. The performance of this linter is so bad that we
       | need to structure our code in a specific way so that we can still
       | afford to run the linter.
       | 
       | Of the performance tips at the end, the interface vs.
       | intersection type one is the suggestion I find the most annoying.
       | That's because it's the most common pattern, and using interfaces
       | is conceptually a lot less clean. It's terrible that a linter
       | effectively forces you into writing worse code.
       | 
       | I really wish the TypeScript team got their act together and
       | fixed the performance of their linter _somehow_. Finding clever
       | optimizations, porting to Go /Rust, whatever is necessary. (3rd-
       | party reimplementations won't do: they'll never catch up with a
       | corporate-funded moving target.)
        
         | seanmcdirmid wrote:
         | Doesn't the type checker have to run in JavaScript to fit into
         | VS code? A C# implementation would be much faster, no need to
         | go native with C++/Rust (not sure speed in Go would really
         | compete).
         | 
         | The big issue is dealing with a structural highly expressive
         | type system, the language of implementation is only going to be
         | a constant slow down (but that constant can be large).
        
           | mook wrote:
           | Don't a lot of the linters run externally via language
           | servers? I'm pretty sure the rust linters are rust-native for
           | example.
        
           | Quothling wrote:
           | It depends on which typescript toolset you use, but generally
           | speaking you're probably riding on mix of C++ and Javascript
           | if you're using VSC. VSC itself uses C++ in it's core
           | components since that is how electron works. Similarly the
           | language server and tooling for both Typescript and Node are
           | build with C++. If you're fancy and use Bun you're running on
           | Zig. Eslint itself runs on Javascript, but the parser it uses
           | feeds it something called an abstract syntax tree, and
           | different parsers will do this differently.
           | 
           | So the relatively simple answer would be no, it would not be
           | faster with C# (or Go which would likely have a similar speed
           | to C#).
        
             | seanmcdirmid wrote:
             | C# should be faster than JavaScript, at least, given all
             | the optimization Microsoft puts into the CLR. But it might
             | not be super portable. I've never seen the typescript type
             | checker before, but I wouldn't be surprised if it were in
             | typescript, which is common as these things go.
        
               | troupo wrote:
               | > But it might not be super portable.
               | 
               | .net is available on all systems where developers work
               | with their code.
        
               | frenchy wrote:
               | But the popular tooling for it (e.g. Visual Studio),
               | isn't.
        
               | troupo wrote:
               | Visual Studio Code is available for Linux and MacOS. As
               | is Rider.
        
         | rtpg wrote:
         | Complicated types are effectively little Prolog programs, doing
         | a bunch of very useful and helpful checks to make sure that
         | your code does what you expect it to.
         | 
         | I do wish that Typescript would offer some tools to make it
         | more ergonomic to write performant unifying code (I kind of
         | despise conditional types, especially when you then use it to
         | create the partially valid types by resolving to never). But I
         | think it would also be very helpful to get people to understand
         | that your types are their own little program that run and have
         | performance characteristics. It's not magic!
         | 
         | There's been some handwaving about performance not being due to
         | it running in JS (because at the end of the day unification is
         | unification is unification and it takes time), but looking at
         | the Typescript codebase in general and poking at it, I can't
         | help but wonder how much of even the heavier stuff is "death by
         | 1000 cuts" on that front.
        
           | vmfunction wrote:
           | > I do wish that Typescript would offer some tools to make it
           | more ergonomic to write performant unifying code (I kind of
           | despise conditional types
           | 
           | Maybe give https://gcanti.github.io/fp-ts/ a go?
        
             | rtpg wrote:
             | I use fp.ts quite a lot! I think it's _extremely
             | unfortunate_ that the API docs don't include top-level
             | examples for basically anything, though. So when people hit
             | a problem that fp-ts or io-ts can solve, I have to
             | basically write a disclaimer about the slight
             | impenetrability before suggesting it.
             | 
             | I really think that TS itself should offer syntax more or
             | less matching what that lib does at the type level, but
             | this is a bit of a maximalist request.
        
               | epolanski wrote:
               | effect-ts is nowadays the de facto successor of fp-ts and
               | has better docs.
               | 
               | As for fp-ts all APIs have examples/tests that show their
               | usage.
        
               | chrisweekly wrote:
               | Thanks for the effect-ts suggestion! Looks pretty
               | compelling...
        
             | shepherdjerred wrote:
             | IMO Zod is much easier to understand than io-ts, the
             | counterpart to fp-ts: https://zod.dev/
        
               | eyelidlessness wrote:
               | It's been a while since I looked deeply at them, but IIRC
               | the primary difference is that io-ts returns Result
               | types. It's trivial to wrap that to produce a value or
               | throw if that feels more comfortable.
               | 
               | It's possible there are other aspects of the APIs that
               | differ in meaningful ways, but last I checked virtually
               | all of the libraries with similar functionality (and
               | there are many) have roughly the same concepts until your
               | schemas themselves get deeply complex.
        
           | scotttrinh wrote:
           | This really was solidified by going through the course at
           | https://type-level-typescript.com since it involves learning
           | the type-level language of TypeScript and solve little
           | puzzles. Doesn't really address performance much, but I think
           | having a working-level understanding of what the type-checker
           | is doing when it's "solving" your TypeScript type-level
           | programs is an important prerequisite for having some
           | intuition about type checker performance.
        
         | Narhem wrote:
         | If I had to guess working with parsing xml is more complicated
         | than traditional code.
         | 
         | Can't find the links but one of the reasons I've seen people
         | move away from xml has been due to the speed in parsing when
         | compared to json or csv.
        
         | rafaelmn wrote:
         | > These days, TypeScript is effectively nothing more than a
         | high-powered linter.
         | 
         | That's a bad take - typescript enables tooling like
         | refactoring/navigation/completion that goes far above a linter.
         | Development tools are just better with typescript vs JS.
        
           | adamc wrote:
           | That's ignoring all the negative effects of using typescript.
        
             | munbun wrote:
             | The only thing you've listed was your preference for
             | readability
        
               | adamc wrote:
               | True, I did not enumerate them. Adds another step to
               | development and generally adds complexity would be other
               | perceived downsides.
               | 
               | I'll note that most of the upsides are also subjective.
        
               | shepherdjerred wrote:
               | Deno/bun support TS natively, and Node is also adding
               | support.
               | 
               | https://github.com/nodejs/node/pull/53725
        
             | rafaelmn wrote:
             | I was refuting the part about typescript (language and the
             | implementation/language server) being a glorified lint -
             | the value proposition is way higher than that.
             | 
             | Even other dynamic languages started moving in this
             | direction due to the benefits of having type annotations
             | brings to tooling/development experience overall.
        
         | k__ wrote:
         | Technically, it is more than a linter, as enums don't have a
         | direct representation in JS.
        
         | bluepnume wrote:
         | At this point TS is a turing complete language.
         | 
         | Complaining that you have to tune it for performance is like
         | complaining that your runtime code isn't automatically
         | maximally performant without a little tuning.
        
           | shepherdjerred wrote:
           | TypeScript has been turing complete for a very long time. You
           | might find this interesting: https://github.com/type-
           | challenges/type-challenges
        
         | epolanski wrote:
         | What's the alternative?
         | 
         | Anything you name has either less support or different cons.
        
           | mdhb wrote:
           | With WASM starting to become a thing we are no longer limited
           | to just JavaScript and things that compile to or transpile to
           | JavaScript.
           | 
           | It's early days there but with the JS ecosystem being the
           | mess that it is I'm actively interested in finding
           | alternatives to at least evaluate.
           | 
           | One approach I'm enjoying so far is Dart which has two
           | relevant compilers (I.e Dart to JS and Dart to WASM) but they
           | have the advantage that you can just use Dart like normal
           | which is a clear 10x improvement over writing either JS or TS
           | and you only have to worry about the specific layers where
           | you need to interop with JS code and you can wrap that up in
           | really nice ways.
           | 
           | For example here's an example of Dart interacting with
           | browser APIs: https://github.com/dart-
           | lang/web/blob/main/example/example.d...
        
           | Quothling wrote:
           | You can use something like JSDoc and achieve basically the
           | same thing, but it's very likely that your developer
           | experience will be way worse as you sort of point out. If
           | you're a VSC enjoyer your tooling will be absolutely horrible
           | compared to the Typescript tooling. We use Typescript as our
           | general JavaScript "language" but most of our internal
           | libraries are written in actual JavaScript for performance
           | reasons. They key difference is those libraries are worked on
           | by far fewer people.
        
             | evilduck wrote:
             | My gut reactions would be to still do whatever performance-
             | related weirdness was absolutely required in a Typescript
             | codebase and either alter the .tsconfig to allow for that
             | project's required style, or to explicitly ts-ignore and
             | type cast the output of the hand-tuned performance code
             | while still maintaining type checking surrounding it so I
             | could still easily produce typedefs for consumers. Even
             | keeping TSC as a type checker while dropping it as a
             | compiler would have been on my list of options before
             | eschewing TSC entirely.
             | 
             | I'm not here to challenge your decisions, but there's a
             | real dearth of information on this topic and knowing when
             | something applies to your situation is hard. Someone
             | writing a web server has different problems and concerns
             | than someone writing React or Vue websites, or someone
             | processing IO on a microcontroller. Can you go into some
             | details about your situational how and why? I'm curious to
             | hear more about the nature of these pure-JS-for-performance
             | libs and what was measured as non-performant in the TSC
             | output?
        
         | xboxnolifes wrote:
         | With such a broad definition of linter, wouldn't the type
         | systems of _all_ statically typed languages just be high-
         | powered linters?
        
           | codeflo wrote:
           | No, in most static languages, the type system influences code
           | generation.
        
           | zachrose wrote:
           | I've worked on several TS projects that don't type check but
           | still "compile" (emit non-TS JavaScript). To me that's the
           | difference between a linter and a compiler, and I wish those
           | projects had stopped compiling when they could no longer type
           | check.
        
             | scotttrinh wrote:
             | Good news! There is a configuration option for that:
             | https://www.typescriptlang.org/tsconfig/#noEmitOnError
        
         | bk496 wrote:
         | There is a new type checker called Ezno that is written in Rust
         | and is a lot faster [1].
         | 
         | I have been tracking PRs like [2] that change the definitions
         | to better be optimised by V8. But the effects are only ~30% and
         | not the 50x that might be achievable by native.
         | 
         | [1]:
         | https://github.com/kaleidawave/ezno/actions/runs/10299707325
         | [2]: https://github.com/microsoft/TypeScript/pull/58928
        
           | IshKebab wrote:
           | How fast does tsc process that input though? I would be very
           | surprised if you can get to 50x faster - that's Python
           | territory and JavaScript isn't that slow. 10x maybe?
        
       | tarasglek wrote:
       | I really wish to find ts tooling that would show me deltas in ts
       | checker memory usage and tie them to diffs in ci
        
         | kevingadd wrote:
         | We accidentally had a regression slip into our TS once that
         | made it take over 7 seconds to typecheck a file, and that was
         | surprisingly painful to diagnose. It meant our CI builds were
         | slower, our local builds were slower, and the language server
         | (in VS code, sublime, etc) would just randomly go unresponsive
         | while editing. If there were tooling to track deltas in that
         | per-file we would have noticed it immediately.
        
           | MonstraG wrote:
           | Can you share what was the problem, just in case we have it
           | too?
        
             | eddd-ddde wrote:
             | I'd bet it had to do with complex templates.
             | 
             | I've had typescript break on me when using libraries like
             | elysia that go full send on their templates.
        
             | kevingadd wrote:
             | We had little strings that encoded call signatures, like
             | "iiff" for int int float float. Someone cooked up a way to
             | have typescript validate the strings at build time, but the
             | way it does typechecks against string literals like that
             | seems to cause every string literal in your entire
             | compilation to get validated in advance to figure out
             | whether it could possibly be that specific type. Each
             | possible value - let's say we had 'f', 'i', and 'l'
             | originally and then we added a fourth option for 'd' -
             | caused the typecheck time to magnify by increasing the
             | possible options. IIRC.
        
       | joseferben wrote:
       | excellent article! my approach is to to break down larger bits
       | into smaller monorepo packages with turbo repo where each package
       | builds itself and the task graph is managed by turbo. the
       | drawback is that watching across local packages doesn't work out
       | of the box.
        
       | zamadatix wrote:
       | Sometimes I wish something akin to Dart (but probably not Dart)
       | had taken off instead of the TypeScript approach. I.e. a JS based
       | language that broke a few things to get types but largely ran on
       | the same VM and could still easily be transpiled in the meantime.
       | Avoid the whole "separate syntax on top of the way the underlying
       | syntax behaves" set of logic.
       | 
       | I suppose WASM enables layered languages like AssemblyScript
       | comes close in many ways but it's also a bit too separated from
       | the primary webpage use case.
        
         | adamc wrote:
         | I learned typescript for a project I spent 9-10 months on. It
         | wasn't that hard to learn but... ugh. On many levels. There are
         | problems where it's typechecking is helpful (enough) for, but
         | the degradation in readability was quite noticeable, and the
         | fact that its abstractions _only_ work at compile time was an
         | endless thorn in the side.
         | 
         | Not a fan.
        
           | shepherdjerred wrote:
           | Zod (and similar libraries) solve that last issue:
           | https://zod.dev/
           | 
           | You can, for example, declare your type and check at runtime
           | if some object matches the type.
        
         | giraffe_lady wrote:
         | In an alternate universe ReScript was this.
        
           | throwitaway1123 wrote:
           | It's a shame that Reason fragmented into Reason and ReScript.
           | I remember there being a ton of excitement around the project
           | when Jordan Walke (creator of React) announced that he was
           | working on a JS successor.
        
         | zarzavat wrote:
         | The TypeScript approach has given us one of the most
         | interesting programming languages to appear in recent times, a
         | language that is completely committed to structural typing.
         | 
         | The Dart approach just gave us yet another unimaginative
         | nominally-typed language.
         | 
         | TypeScript is complex, but it's also incredibly cool if you're
         | into compilers. Engineers do their best work when there are
         | limitations imposed, in this case the need to add types to JS.
        
       | scotttrinh wrote:
       | Hey, article's author here! Happy to answer any questions, or
       | poke at this general problem with anyone who is interested.
       | Understanding the type checker and its performance is my current
       | personal focus and I find it helpful to bat around ideas with
       | others.
        
         | timcobb wrote:
         | Hey thanks so much for writing this up. This is a great post!
         | I've only had time to skim the article, so my apologies if you
         | covered this and I missed it: have you investigated whether
         | specifying expression/function return type affects performance?
         | I work on something of a large codebase, and I wonder if
         | whether we annotated our returns, type checking would be
         | faster.
        
           | scotttrinh wrote:
           | Yeah! Making explicit return types, especially of public
           | functions, is a good practice to follow. I'd say the main
           | reason isn't performance, though, but rather to ensure you
           | have a stable public API for your module.
           | 
           | How much it actually speeds up the type checker depends on
           | how hard it is for the type checker to infer the return type.
           | And that depends on the return expression, but I don't think
           | there is a single hard and fast rule here. But, if you
           | already have a named type for the return value of the
           | expression, I would absolutely annotate it explicitly when
           | possible. Sometimes the inferred type won't be really the
           | type you intend, and there might just be a more clear type
           | you want to use for communication/documentation purposes.
        
       | brundolf wrote:
       | I've come to believe that sufficiently advanced type inference is
       | indistinguishable from an interpreter
       | 
       | ...which has the implication that what TypeScript is actually
       | giving us is a REPL. Our code is increasingly "evaluated" by our
       | IDE, in our hover-overs
       | 
       | I think this is a major reason people like TypeScript so much
        
         | shepherdjerred wrote:
         | TypeScript's type system is turing complete. You might find
         | this interesting: https://github.com/type-challenges/type-
         | challenges
        
       ___________________________________________________________________
       (page generated 2024-08-14 23:01 UTC)