Tips for Performant TypeScript
github.com
github.com
- union types aren't always bad, DON'T avoid them
- DON'T feel the need to annotate every single thing
Please apply a critical lens as you read through this page. The document was meant for users who've hit rough perf issues, and we tried pretty hard to explain the nuances of each piece of advice.
I was wondering whether any more could be done to improve editing performance when large .d.ts files are included. This is a problem in particular for NativeScript, which has a vast set of large types files to include to express the entirety of the iOS and Android SDKs, e.g.: https://github.com/NativeScript/NativeScript/tree/master/pac...
In fact, the skipLibCheck flag was originally developed to improve NativeScript compile time: https://github.com/microsoft/TypeScript/issues/8521
Unfortunately, editing still feels slow to me when including NativeScript’s iOS/Android types (have to wait 1-2 seconds after any keystroke for any IntelliSense to appear); beyond including fewer of the types files, could editing performance be improved somehow?
Thanks!
However, I was surprised that "performance" refers to compilation/editing speed rather than execution speed.
But you already used the words naturally in your sentence, "compilation speed". A quick google search brought up lots of useful results with 'typescript compiles slow', 'typescript compile faster', etc.
A personal app I am working on is about 2mb of TypeScript now and takes about 6.5s to compile on my laptop.
The other two code-related sections seem odd, to write code that improves compile-time performance. It would be beneficial to see compile duration differences between projects that heavily use union types and projects that don't. Otherwise, changing your coding style and not using explicit features of a language that are hard to find in other languages seems counter-productive.
That said, the actual compiler configuration changes that follow seem very useful, from someone who doesn't write much TS.
The only time I see manual type annotation cause problems consistently is with React.FC<Props>: (props: T). People don’t always remember to provide their props interface as the generic, and instead directly annotate the props argument of the function. This is a subtle issue that breaks the “magic” props added by React (like children), leading to people adding their own children definitions to their props interfaces d’oh
IMO this is a feature not a bug. Type definitions aren't just for the compiler, they're also for the developer. Being able to see at a glance which components expect children and which don't is really valuable. Not to mention that there are situations where I want to restrict what kinds of children can be passed in (think render-props, or named slot projection patterns).
In other words, just because React supports an implicit definition of what a "child" can be doesn't mean that my specific component supports all of those same possibilities.
Maybe for you my own projects I’ll employ your approach, because I agree from the fundamentals side of it
As with most optimization suggestions, I take these to be intended as a remedy when you're actually running into problems, not something to be done eagerly. I've never run into significant cross-project TypeScript performance issues personally, but I have heard of that happening to some people.
> The earlier on these practices are adopted, the better.
So you get minimal keyboard typing when passing around inline closures -- don't have to write the types yourself or maintain them as the code changes -- and any changes to the inferred return types would be visible in source control with diffs that provide quite rich information about changes that might have done something unexpected or propagated further than realized
faster compilations and editing experiences
i.e. not runtime perf.
I don't think you could give general advice on what is slower as that's a constantly moving target.
Looks like assemblyscript compiles "a strict variant of TypeScript" directly to Wasm:
https://github.com/AssemblyScript/assemblyscript
This is pretty close to "running" TypeScript.
Never heard of AssemblyScript, but yeah, it or a statically typed language like it is where frontend development needs to go. In concordance with my point about TS being fundamentally unsuitable, the AssemblyScript docs (https://www.assemblyscript.org/basics.html#strictness) say:
> WebAssembly is fundamentally different from JavaScript, ultimately enabling entirely new use cases not only on the web. Consequently, AssemblyScript is much more similar to a static compiler than it is to a JavaScript VM. One can think of it as if TypeScript and C had a somewhat special child.
> Unlike TypeScript, which targets a JavaScript environment with all of its dynamic features, AssemblyScript targets WebAssembly with all of its static guarantees, hence intentionally avoids the dynamicness of JavaScript where it cannot be compiled ahead of time efficiently.
[0] -- https://deno.land/
"It's just a type checker for JavaScript" - not really. It's a language that transpiles to JavaScript and it happens to be a superset of JavaScript (namespaces and enums, anyone?).
But saying that it's just a layer on top of JavaScript sounds like saying C++ is just a layer on top of C.
C++ used to be just a layer on top of C, but not anymore. If it still was, it wouldn't be a bad thing to say.
Type checks that run during compilation/transpilation/typechecking do not "run" as the code is executed. This is what happens with Typescript, and that's what the original commenter was saying.
When people talk about C performance, they mean the performance of the code written in C and (ultimately) compiled to machine language. Very seldom they mean the speed of compilation (though they sometimes do, in which case they almost always explicitly mention "compilation").
When people talk about Typescript performance, they either mean Javascript performance or, more likely, the speed with which Typescript type-checks their code; the latter doesn't happen in run time. That's what the original commenter meant when they said "you cannot run Typescript". It's also what TFA means by "performant".
Right. One of the first things you learn while doing TypeScript is that interfaces don't exist after compile time. So in general, it's better to write interfaces and use plain-old-objects than to use `class`, which generates real JavaScript code.
Within reason, of course. Classes are still useful. But it's not necessarily the first thing to reach for.
- import { otherFunc } from "other";
+ import { otherFunc, otherType } from "other";
- export function func() {
+ export function func(): otherType {
return otherFunc();
}
Not manually annotating the return type here reduces both the work you need to do when refactoring and the visual overload when working with the code. In my opinion, both of those are far more important than small changes in compile time.[0] https://github.com/microsoft/TypeScript/wiki/Performance#usi...
I'm not sure I understand, could you elaborate more?
Can’t speak to its quality, but there’s nothing stopping someone from writing a repl from a sufficient compiler API... and “good” is only limited by inference quality and runtime performance. IDEs are pretty snappy at showing you autocomplete as you type regardless of whether the language has a garbage collector. And performance - well, that’s what caching would be for, and an optimized compiler design that only needs to recompile changed code...
I would also point out the “auto” keyword in CPP likely saves folks a lot of typing ;-) I know it and similar inferences changed my mind on the whole static vs dynamic debate...
So I think this is more of a tooling issue, do keep in mind global type inference is really handy for interactive programming in the repl and short scripts.
Type inference
One thing that I'm fascinated and terrified by is the global type inference.
Doesn't it get really hard to figure how how you're allowed to call things?
Does it make your IDE experience slow? Similar to one of the things mentioned in the OP, I would think that the type hints would be super helpful to the compiler/analyzer.
As I mentioned earlier it's a powerful feature, and has its uses, especially when you are still trying to figure the types of your program.
For an easier taste of global type inference you can try elm language, it's also a good stepping stone to learning haskell
If I didn’t declare the return type then the function will silently now infer the return type to be “string | number” (which might or might not break compilation elsewhere).
Types are a subset of contracts. Contracts are best explicit at the API boundary - which is to say, on exported functions and members of exported classes.
const foo: Foo = new Foo();The return type is part of the contract for what a method does. Changing it is just as dangerous as changing the type on a method parameter.
I agree that using type inference locally is a boon to productivity. I disagree that it's a negative for a method return parameter.
Keeping it fixed prevents a future refactor from breaking the method expectations and contracts down the line.
For me this also greatly improves my ability to refactor quickly and confidently (not needing to check additional places/files during refactor for edge cases in expected type).
Yes any type-related refactor mistakes should in theory be picked up at compile time, or by smart IDEs but having the context in front of you still greatly improves speed when writing.
It's also great for code reviews where grokking context is trickier and IDE goodies are typically not as rich.
this is something VSCode/editors could always show if desired via secondary notation, without requiring annotations.
I've already mentioned code reviews as a good example of contexts where we need to read code outside of an IDE regularly, but even beyond that, a language's readability shouldn't be dependent on using some specific environment to read it.
I used to be a language purist, but nowadays the costs of not using an ide or lsp supported tools is just too high. I’d prefer minimal tokens and abundant secondary notations provided by parsers than having to add clunky syntax myself.
Maybe I'm in a minority but calling anyone using type annotation "language purists" seems a bit of an extreme classification.
So IDEs first but never violate the rule that it's easy to fix something in vim or notepad in a pinch.
On the one hand, I'm like you. I don't want to be forced to use a specific tool to work with a programming language.
But I can't quite put my finger on why. Like, what if we just say "language Foo includes a compiler and the working environment is this IDE"? Is that wrong? It kinda feels wrong. But that's what Smalltalk does, right? Maybe the tight integration would actually be better.
This statement makes me think... how come the TS compiler is not using something like a hash/map (object) of union members to basically ignore redundancy?
Or any other strategy really. The union of unique values in 2 or more arrays is a classic CS problem for which there are many, many performant solutions.
Anyone familiar with the TS internals? Maybe I'm not seeing the forest.
The trouble is the operation isn't "is X a member of Y", rather it's "does X match any values of Y according to predicate P."
You can break that out if you have knowledge of possible X's and P, as is the case with type matching.
Say we are checking G[] against {str, "foo literal", int[]}. I have no idea how TS implements these internally, but say the underlying type expressions are:
[{head: String},
{head: String, constraint:"foo literal"},
{head: Array, element:{head: Integer}}]
And G[] is {head:Array, element: {head: Generic, name: "G"}}.We could reasonably require that heads are all simple values, and then group types as such:
{String: [{head: String},
{head: String, constraint:"foo literal"}],
Array: [{head: Array, element: {head: Integer}}]}
You'd still have to try to match that generic parameter against all the possible Arrays, but you could add more levels of hashing.The downside is, of course, it's quite tricky to group types like this and prove that it returns the same results as checking all pairs, especially when you have a complex type system.
The only reason I've used VS Code for more than an hour in the last few years is because its Svelte plugin was much better, but now JetBrains has a good Svelte plugin, and I'm back to JetBrains 100% of the time. It's worth every penny.
VSCode is fantastic in many ways, and I'll keep using for my markdown dev notes and for general purpose programming occasionally, but for anything of substance I'm a JetBrains convert.
We're beginning to see more Javascript tooling that are written in other languages such as Go and Rust that just blow away existing tooling in terms of performance. Look up on esbuild/swc.
Last I heard JetBrains is also looking into adopting LSP so maybe we could even pay for JetBrains LSP to use with VSCode some day
I think I'll largely continue to write my TypeScript in the way that seems best to me using the syntax provided to me, and let MS hopefully optimise these issues under the hood over time.
I’m planning to give Emotion a try as it has the same ‘styled’ api.
I've never had issues with the TypeScript compiler being slow, and I write my types with as much complexity as the underlying data/situation demands.
I have several projects that consume dozens of REST APIs, and I have types for all of their requests and responses, and my compilation time is still fine.
With that said, compilation performance is something most compiled languages deal with so I wouldn’t treat it as some kind of flaw specific only to TypeScript
That page can give some guidance on diagnosing what the compiler's time is going into. Most users won't need to do this, but there are plenty of bigger teams/organizations that are really invested into TS and do want to keep their build times lean.
The worst turned out to be a crashing bug in tsserver that manifested as autocomplete and error checking feeling sluggish, I think because the language service kept restarting. It made me feel grumpy about TS for a month or two before I finally looked at the server logs (using the instructions on this page). That quickly led me to this bug: https://github.com/microsoft/TypeScript/issues/35036. Even though it took a while to get fixed, I felt immensely better that the problem was real, acknowledged, etc.
Nowadays my biggest performance issue is with the Material-UI typings.
Can we actually identify which piece takes longer to compile?
I think you're misunderstanding this situation based on the suggestive word, "compiler".
TypeScript's type system is Turing-complete. That means you can theoretically write programs with the type system that cause the compiler to never complete.
Instead of thinking about this as optimizing a compiler, think of it as optimizing runtime performance of a program by better understanding the language that the program is written in.
I'm sure you've already spent time on this, but it piques my interest that there isn't a good solution to the issue.
Some of these things can be used, yes, those that don't change the approach to the project absolutely, but otherwise ... stick with 'the right pattern' for the architecture.