TypeScript 5.5 release candidate
A couple days ago, the TypeScript team announced the TypeScript 5.5 release candidate.
Looks like it’ll have some interesting stuff, so let’s take a look.
Inferred type predicates
TypeScript’s control flow analysis does a great job of tracking how the type of a variable changes as it moves through your code
const bird = nationalBirds.get(country); // Bird | undefined
if (bird) {
bird.sing(); // Bird
} else {
// undefined
}
This is showing something pretty great about TypeScript. Even though `bird` can be undefined, it knows where we’ve checked for its existence and adjusts the type accordingly.
One place this doesn’t work as nicely is when filtering items out of a list.
// birds: (Bird | undefined)[]
const birds = countries
.map(country => nationalBirds.get(country))
.filter(bird => bird !== undefined);
for (const bird of birds) {
bird.sing(); // error: 'bird' is possibly 'undefined'.
}
This code is just as “safe” as the previous example, but TypeScript doesn’t know that.
Until TypeScript 5.5 that is!
This works because TypeScript now infers a type predicate for the
filter
function.
We could’ve always extracted a function called a type predicate to help with this
function isBirdReal(bird: Bird | undefined): bird is Bird {
return bird !== undefined;
}
The magic is that now TypeScript will infer the type predicate part. We don’t need to explicitly write it. This includes our inline function passed to `filter`.
There are some common-sense rules that you’ll have to follow for TypeScript to infer the type predicate. Check out the announcement to see them!
Control flow narrowing for constant indexed access
TypeScript is now able to narrow expressions of the form
obj[key]
when bothobj
andkey
are effectively constant
const obj: Record<string, unknown> = ...
if (typeof obj[key] === "string") {
// Now okay, previously was error
obj[key].toUpperCase();
}
This is similar to inferred type predicates. We know what we’re doing is safe, but TypeScript hasn’t followed along.
Now it’ll understand that even though we’re accessing arbitrary keys and values, we’ve typechecked its type at runtime and haven’t modified it, and can safely assume the type of the value.
Type imports in JSDoc
Today, if you want to import something only for type-checking in a JavaScript file, it is cumbersome.
/**
* @param {import("./some-module").SomeType} myValue
*/
function doSomething(myValue) {
// ...
}
Here we’re importing a type into a JSDoc comment in a JS file. This works great, but has to be repeated anywhere SomeType is used. Or you can use `@typedef`, which can also be clunky.
That’s why TypeScript now supports a new
@import
comment tag that has the same syntax as ECMAScript imports.
/** @import { SomeType } from "some-module" */
/**
* @param {SomeType} myValue
*/
function doSomething(myValue) {
// ...
}
What I like about this is that importing the type is very close to normal TypeScript code. There’s less JSDoc-specific stuff to remember.
Regular expression syntax checking
Until now, TypeScript has typically skipped over most regular expressions in code.
This is because regular expressions technically have an extensible grammar and TypeScript never made any effort to compile regular expressions to earlier versions of JavaScript.
Still, this meant that lots of common problems would go undiscovered in regular expressions, and they would either turn into errors at runtime, or silently fail.
I did not know that TypeScript doesn’t really look at most regular expressions, but it makes sense. It’s a whole other syntax/language/grammar.
But TypeScript now does basic syntax checking on regular expressions!
let myRegex = /@robot(\s+(please|immediately)))? do some task/;
// ~
// error!
// Unexpected ')'. Did you mean to escape it with backslash?
It’s pretty nice that TypeScript will now find common mistakes, including synta errors and issues around back references and named capture groups that don’t exist.
Performance optimization
Don’t know about you, but I have no idea what monomorphization is…
In TypeScript 5.5, the same monomorphization work has been done for the language service and public API.
What this means is that your editor experience, and any build tools that use the TypeScript API, will get a decent amount faster. In fact, in our benchmarks, we’ve seen a 5-8% speedup in build times when using the public TypeScript API’s allocators, and language service operations getting 10-20% faster.
While this does imply an increase in memory, we believe that tradeoff is worth it and hope to find ways to reduce that memory overhead. Things should feel a lot snappier now.
What I’m getting out of this is… we’re monomorphizing the crap out of more stuff and things are faster. Always happy to see that!
Conclusion
TypeScript is very popular and lots of folks really appreciate what it brings to the frontend development experience.
Not everyone, though. I do talk to people who either:
Prefer the dynamic nature of not having types
Or go the opposite way and are frustrated when TypeScript isn’t type safe enough or doesn’t exactly understand their code
Both miss the point that TypeScript leans into being “just JavaScript”, including it’s dynamic nature, while also providing most of the benefit of strict types but with less effort.
What this release shows is that the TypeScript team is incrementally, month after month, year after year, making TypeScript more type-safe for more JavaScript patterns 🤩
Want to talk shop or work together? Get in touch at landslide.software.