Back

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

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

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.

replayer.png

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 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:

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?