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