What's new in TypeScript 4.4
On August 26th, 2021, we got the announcement of TypeScript 4.4, and as usual, I’m going to highlight everything new and every cool addition to our favorite superset!
Control flow analysis now works with variables#
When we use TypeScript, one of the big fallacies a lot of people bring up as a blocker is having to keep declaring types for every single piece of data you have. That’s not true.
The TS compiler is powerful enough to understand the control flow of your code, so it knows when a variable or some other piece of data is of a specific type based on a check made earlier. That check is commonly called a type guard. It’s when we do something like this:
function foo (bar: unknown) { if (typeof bar === 'string') { // TS now knows the type is String console.log(bar.toUpperCase()) }}This holds true not only for unknown but also for generic types like any.
The big problem was that if we moved that check into a constant or a function, TS lost track of the flow and couldn’t figure out what was going on anymore, for example:
function foo (bar: unknown) { const isString = typeof bar === 'string' if (isString) console.log(arg.toUpperCase()) // ~~~~~~~~~~~ // Error! Property 'toUpperCase' does not exist on type 'unknown'.}Now, TS can identify the constant and its return, and it can deliver the result without errors. The same works for complex types, or discriminant types:
type Animal = | { kind: 'cat', meow: () => void } | { kind: 'dog', woof: () => void }
function speak (animal: Animal) { const { kind } = animal
if (kind === 'cat') { animal.meow() } else { animal.woof() }}Inside the types extracted through destructuring, we now get the correct string assertion. Another cool thing is that it also understands transitively how all the types work, meaning it goes type by type to infer the current type of the object based on the checks you’ve already made:
function f(x: string | number | boolean) { const isString = typeof x === "string" const isNumber = typeof x === "number" const isStringOrNumber = isString || isNumber if (isStringOrNumber) { x // Type of 'x' is 'string | number'. } else { x // Type of 'x' is 'boolean'. }}Index signatures with Symbols and templates#
There’s a type called an index signature. Basically, this type tells us that the object in question can have arbitrarily named keys, like a dictionary, represented as [key: string]: any.
The only possible types for an index signature right now are string and number, because those are the most common types.
There’s another type called Symbol, though, which is heavily used, especially by people building libraries, to index the types of their arrays and objects without having to expose or modify them. With 4.4 landing, you can now do this:
interface Colors { [sym: symbol]: number;}
const red = Symbol("red");const green = Symbol("green");const blue = Symbol("blue");
let colors: Colors = {};
colors[red] = 255;let redVal = colors[red];It was also impossible to have a subset of string or number, like template string types, as keys. For example, an object whose keys always start with data-. Now that’s fully valid:
interface DataOptions { [key: `data-${string}`]: unknown}
let b: DataOptions = { "data-foo": true "qualquer-coisa": true, // Error! 'unknown-property' wasn't declared in 'DataOptions'.};Catch now defaults to unknown#
As a lot of people know (and complained about!), when we use a try/catch inside any function in TypeScript, the catch block always takes an error parameter that, by definition, had type any.
After some discussions with the community about what the correct type should be, a lot of people leaned toward unknown as the default type for errors. Leaving the type wide open as any essentially gives you no typing at all. So TS 4.4 introduces a new tsconfig option and a new flag called useUnknownInCatchVariables, which is off by default so it doesn’t break compatibility, but can and should be turned on.
try { codigo();}catch (err) { // err: unknown
// Error! Property 'message' does not exist on type 'unknown'. console.error(err.message);
// Define o tipo de erro if (err instanceof Error) { console.error(err.message); }}If you turn on the strict flag, this one gets turned on too.
Exact optional properties#
Another problem the community brought up was the conflict between optional properties declared as prop?: <type>. This kind of property gets expanded to prop: <type> | undefined, but what if the property could actually hold an undefined value on purpose?
So if someone wanted to write an optional property of type number as undefined, that was fine by default, but it caused a bunch of problems:
interface Pessoa { nome: string idade?: number}
const Lucas: Pessoa = { nome: 'Lucas', idade: undefined } // okAnd this practice leads to a bunch of bugs because we end up treating a valid value the same as a nonexistent one. Even more so if we had to handle the idade property at some point. On top of that, each method like Object.assign, Object.keys, for-in, for-of, JSON.stringify, and so on, handles the “property exists or not” case differently.
In version 4.4, TS adds a new flag called exactOptionalPropertyTypes, which makes this error go away, since you won’t be able to use undefined on a property typed as optional anymore.
interface Pessoa { nome: string idade?: number}
const Lucas: Pessoa = { nome: 'Lucas', idade: undefined } // ErroJust like the previous one, this property is part of the strict set.
Support for static blocks#
ECMA2022 has a new feature planned called static initialization blocks. This feature will let us write more complex initialization code for static members of a class. We’ll talk more about this here on the blog soon!
For now, though, TS 4.4 already supports this feature.
Wrap-up#
These were the most important changes in TS 4.4, but not the only ones. We also got a bunch of performance improvements, plus better readability and VSCode integration.