TypeScript 4.4: The Good, The Bad and The Not So Bad
On August 12th, TypeScript 4.4 Release Candidate (RC) was announced. This means that the changes have been finalized, and an official, stable release is coming shortly after a few additional bug fixes.
Let’s go through what’s new, how it could impact your development experience, and how to give it a try right now!
Breaking changes
We’ll start by summarising the breaking changes. In this release, there are a few of them that aren’t major but could still break your code:
- You can no longer initialize
abstract
properties insideabstract
classes. You can only define their type. Promise
checks have been improved. TS will now remind you to includeawait
when you’reif
-checking thePromise
results in more cases than before to prevent unintended always-truth checks.catch
parameters are nowunknown
instead ofany
by default when either\--strict
or new\--useUnknownInCatchVariables
flag is on.this
value is disregarded when calling imported functions to align with ECMAScript Modules specification on all available module systems (ESM, AMD, CommonJS, etc.)lib.d.ts
has changed to be in line with current specifications (especially thelib.dom.d.ts
with all changes noted here)
With that out of the way, let’s see what big new features you can expect!
Improved type guard detection
Probably the most important feature of TypeScript 4.4 is “control flow analysis of aliased conditions and discriminants”.
That means that from now on, aliased type guards, even with discriminated unions, will be properly analyzed and used for narrowing the given type.
In previous TS versions, the following wouldn’t work.
const example = (arg: string | number) => {
const isString = typeof arg === "string";
if (isString) {
return arg.toUpperCase(); // Error
}
return arg.toPrecision(); // Error
};
The isString
condition wasn’t able to let TS know that, when it’s true, the arg
is a string
. As a result, TS gave an error when you used type-specific methods and properties, still thinking that arg
‘s type is string | number
. Only placing the condition in the if
statement directly was properly interpreted.
With improved control flow analysis, that will no longer be an issue in TS 4.4. Furthermore, TS will also detect proper types when dealing with discriminated unions, even when performing checks on destructed properties!
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; sideLength: number };
const area = (shape: Shape): number => {
const { kind } = shape;
const isCircle = kind === "circle"; // shape.kind === "circle" will also work
if (isCircle) {
// Circle
return Math.PI * shape.radius ** 2;
}
// Square
return shape.sideLength ** 2;
};
Up to a certain depth, TS will also recognize more complex, combined conditions and narrow the type accordingly.
const example = (x: string | number | boolean) => {
const isString = typeof x === "string";
const isNumber = typeof x === "number";
const isStringOrNumber = isString || isNumber;
if (isStringOrNumber) {
x; // string | number
} else {
x; // boolean.
}
};
These improvements are really great! TS developers will now be able to nicely layout and annotate complex conditions without putting everything in their if
statements or using direct type assertions.
More versatile index signatures
Another great improvements have to do with index signatures. You’ll no longer be limited to just number
and string
. Now, symbol
and template string patterns will also be allowed.
interface Colors {
[sym: symbol]: number;
}
const red = Symbol("red");
const green = Symbol("green");
const blue = Symbol("blue");
const colors: Colors = {};
colors[red] = 255;
colors[red]; // number
colors[blue] = "da ba dee"; // Error
colors["blue"]; // Error
symbol
index signatures are a nice addition. However, in my opinion, template string pattern index signatures are much more interesting! This will let you narrow the index signature to a certain pattern, allowing for complex type definitions like never before!
interface Example {
a: number;
b: string;
[prop: `data-${string}`]: string;
}
const test1: Example = {
a: 1,
b: "example",
"data-test": "example",
};
const test2: Example = {
"data-test": "example",
}; // Error (no "a" and "b")
const test3: Example = {
a: 1,
b: "example",
test: "example",
}; // Error ("test" not accepted)
If you’ve ever wanted to use index signature but narrow it down from general string
, this update will be huge for you!
On top of all that, union index signatures will also be allowed. Any combination of string
, number
, symbol
, and string template pattern is acceptable.
interface Example {
[prop: string | number]: string;
}
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
Exact optional property types
Apart from \--useUnknownInCatchVariables
, one more flag has been introduced - \--exactOptionalPropertyTypes
.
With this flag turned on, TS will no longer allow initializing optional properties with undefined
.
interface Example {
a: string;
b?: number;
}
const test: Example = {
a: "example",
b: undefined, // Error if --exactOptionalPropertyTypes is turned on
};
Such behavior determining whether the property actually is present on the object (with undefined
value or otherwise) is useful in several cases.
When using, e.g., Object.assign
, or object spread ({ …obj }
), properties with undefined
are actually handled differently compared to truly non-existent properties. Depending on the implementation, the same can be true for your code as well.
To allow for undefined
with \--exactOptionalPropertyTypes
turned on, you’ll have to explicitly include undefined
in a union type. Without the flag, such behavior is automatic.
interface Example {
a: string;
b?: number | undefined;
}
const test: Example = {
a: "example",
b: undefined, // Works correctly (even with --exactOptionalPropertyTypes on)
};
Because this flag can cause issues to arise in both your code and 3rd-party definitions (e.g., from DefinitelyTyped), it’s not included with \--strict
and thus is opt-in and non-breaking.
If you feel like that could help in your codebase, turn on this flag, along with \--strictNullChecks
to opt-in.
Static blocks in classes
The last big new feature are static
blocks.
It’s an upcoming ECMAScript feature that’s currently a stage 3 proposal. static
blocks allow for a more complex initiation process of static
class members.
class Example {
static count = 0;
// Static block
static {
if (someCondition()) {
Example.count++;
}
}
}
Although the above was already possible, this feature makes the process simpler and much more elegant by allowing for the initiation block to be right inside the class definition.
Previously, such logic had to be put outside of the class definition, making it feel separate and cumbersome.
class Example {
static count = 0;
}
if (someCondition()) {
Example.count++;
}
Apart from that, static
blocks also have the advantage of allowing access to private static and instance fields (given how they’re part of the class’ definition), providing an opportunity for sharing their values with other classes or functions available in the same scope.
let exampleFunc!: (instance: Example) => number;
class Example {
static #accessCount = 0;
#initial: number;
constructor(input: number) {
this.#initial = input * 2;
}
static {
exampleFunc = (instance: Example) => {
Example.#accessCount++;
return instance.#initial
}
}
}
if (exampleFunc) {
exampleFunc(new Example(2)); // 4
}
Performance improvements
Aside from new features and breaking changes, as always, there are a few notable performance improvements:
- Faster declaration emits thanks to additional caching.
- Conditional path normalization reduces the time it takes the compiler to normalize paths it’s working with, thus loading faster.
- Faster path mapping for
paths
intsconfig.json
, thanks to additional caching, brings significant performance improvements. - Faster incremental builds with
\--strict
thanks to fixing a bug causing unnecessary type-checking on every subsequent build. - Faster source map generation for big outputs
- Faster
\--force
builds thanks to reduced unnecessary checks
Intellisense improvements
The area that TS is most well-known for - intellisense (aka autocompletion/editor support) has also seen some improvement.
As TS suggestions get more confident, from 4.4, it’ll automatically issue spelling suggestions for pure JavaScript files without checkJs
, or @ts-check
turned on. These will be non-invasive, “Did you mean…?” type of suggestions.
For other visible improvements, TS can now show inline hints, aka “ghost text”. This can apply to everything from parameter names to inferred return types.
Also improved are suggested import paths. Instead of unruly, relative paths like node_modules/…
, TS will display paths you actually use - like react
instead of node_modules/react/..
or something. A cosmetic, but welcomed change.
Test drive
With all these great features, you’re likely wondering when you’ll be able to use them. Your best bet would be to wait until stable release in the next few weeks. This way, you won’t have to deal with unresolved bugs and other issues.
However, if you want to test drive the RC version right now, you can get it from NPM:
npm install typescript@rc
Then, if necessary, select it for use in your IDE/code editor of choice.
Naturally, VS Code would provide you with the best experience, and with VS Code Insiders, you’ll get the latest TS version out-of-the-box.
Bottom line
So there you have it! Tons of improvements are coming with TS 4.4, and even more, are already planned.
If you’re a TS user, this will be a good update for you. It’ll for sure improve your development experience even further than TS already does. And if you’re not already using TS, maybe it’s the right time for you to give it a try?