OpenReplay
Navigate back to the homepage
BLOG
Browse Repo
Back

TypeScript 4.4: The Good, The Bad and The Not So Bad

Arek Nawo
August 17th, 2021 · 4 min read

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 inside abstract classes. You can only define their type.
  • Promise checks have been improved. TS will now remind you to include await when you’re if-checking the Promise results in more cases than before to prevent unintended always-truth checks.
  • catch parameters are now unknown instead of any 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 the lib.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.

1const example = (arg: string | number) => {
2 const isString = typeof arg === "string";
3
4 if (isString) {
5 return arg.toUpperCase(); // Error
6 }
7
8 return arg.toPrecision(); // Error
9};

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!

1type Shape =
2 | { kind: "circle"; radius: number }
3 | { kind: "square"; sideLength: number };
4
5const area = (shape: Shape): number => {
6 const { kind } = shape;
7 const isCircle = kind === "circle"; // shape.kind === "circle" will also work
8
9 if (isCircle) {
10 // Circle
11 return Math.PI * shape.radius ** 2;
12 }
13 // Square
14 return shape.sideLength ** 2;
15};

Up to a certain depth, TS will also recognize more complex, combined conditions and narrow the type accordingly.

1const example = (x: string | number | boolean) => {
2 const isString = typeof x === "string";
3 const isNumber = typeof x === "number";
4 const isStringOrNumber = isString || isNumber;
5
6 if (isStringOrNumber) {
7 x; // string | number
8 } else {
9 x; // boolean.
10 }
11};

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.

1interface Colors {
2 [sym: symbol]: number;
3}
4
5const red = Symbol("red");
6const green = Symbol("green");
7const blue = Symbol("blue");
8const colors: Colors = {};
9
10colors[red] = 255;
11colors[red]; // number
12colors[blue] = "da ba dee"; // Error
13colors["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!

1interface Example {
2 a: number;
3 b: string;
4 [prop: `data-${string}`]: string;
5}
6
7const test1: Example = {
8 a: 1,
9 b: "example",
10 "data-test": "example",
11};
12const test2: Example = {
13 "data-test": "example",
14}; // Error (no "a" and "b")
15const test3: Example = {
16 a: 1,
17 b: "example",
18 test: "example",
19}; // 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.

1interface Example {
2 [prop: string | number]: string;
3}

Open Source Session Replay

Debugging a web application in production may be challenging and time-consuming. OpenReplay is an Open-source alternative to FullStory, LogRocket and Hotjar. It allows you to monitor and replay everything your users do and shows how your app behaves for every issue. It’s like having your browser’s inspector open while looking over your user’s shoulder. OpenReplay is the only open-source alternative currently available.

OpenReplay

Happy debugging, for modern frontend teams - Start monitoring your web app 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.

1interface Example {
2 a: string;
3 b?: number;
4}
5
6const test: Example = {
7 a: "example",
8 b: undefined, // Error if --exactOptionalPropertyTypes is turned on
9};

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.

1interface Example {
2 a: string;
3 b?: number | undefined;
4}
5
6const test: Example = {
7 a: "example",
8 b: undefined, // Works correctly (even with --exactOptionalPropertyTypes on)
9};

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.

1class Example {
2 static count = 0;
3
4 // Static block
5 static {
6 if (someCondition()) {
7 Example.count++;
8 }
9 }
10}

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.

1class Example {
2 static count = 0;
3}
4
5if (someCondition()) {
6 Example.count++;
7}

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.

1let exampleFunc!: (instance: Example) => number;
2
3class Example {
4 static #accessCount = 0;
5 #initial: number;
6 constructor(input: number) {
7 this.#initial = input * 2;
8 }
9
10 static {
11 exampleFunc = (instance: Example) => {
12 Example.#accessCount++;
13
14 return instance.#initial
15 }
16 }
17}
18
19if (exampleFunc) {
20 exampleFunc(new Example(2)); // 4
21}

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 in tsconfig.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.

TypeScript inline hints

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.

Improved suggested import path

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:

1npm 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?

More articles from OpenReplay Blog

What are Higher-Order Components in React?

A function that takes as an argument a component and returns a new component, but why would you do that?

August 12th, 2021 · 8 min read

Forever Functional: Injecting for Purity

Pure functions are the cornerstone of Functional Programming, but we must also learn how to deal with impurity in our code

August 10th, 2021 · 7 min read
© 2021 OpenReplay Blog
Link to $https://twitter.com/OpenReplayHQLink to $https://github.com/openreplay/openreplayLink to $https://www.linkedin.com/company/18257552