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.
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.
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.
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.
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.
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.
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.
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.
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.
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.)
> 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.
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.
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?
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.
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.
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.
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.
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.
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.
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).
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#).
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.
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.
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.
Not a fan.
You can, for example, declare your type and check at runtime if some object matches the type.
...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
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.)
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.
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
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.
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.
Maybe give https://gcanti.github.io/fp-ts/ a go?
Anything you name has either less support or different cons.
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...
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.
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).
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#).