Announcing TypeScript 4.4 Beta

Avatar

Daniel

Today we are excited to announce the beta release of TypeScript 4.4!

To get started using the beta, you can get it through NuGet, or use npm with the following command:

npm install typescript@beta

You can also get editor support by

Some major highlights of TypeScript 4.4 are:

Let’s explore these in more detail!

Control Flow Analysis of Aliased Conditions

In JavaScript, we often have to probe a variable in different ways to see if it has a more specific type that we can use. TypeScript understands these checks and calls them type guards. Instead of having to convince TypeScript of a variable’s type whenever we use it, the type-checker leverages something called control flow analysis to deduce the type within every language construct.

For example, we can write something like

function foo(arg: unknown) {
    if (typeof arg === "string") {
        // We know this is a string now.
        console.log(arg.toUpperCase());
    }
}

In this example, we checked whether arg was a string. TypeScript recognized the typeof arg === "string" check, which it considered a type guard, and was able to determine that arg should be a string in the body of the if block.

However, what happens if we move the condition out to a constant?

function foo(arg: unknown) {
    const argIsString = typeof arg === "string";
    if (argIsString) {
        console.log(arg.toUpperCase());
        //              ~~~~~~~~~~~
        // Error! Property 'toUpperCase' does not exist on type 'unknown'.
    }
}

In previous versions of TypeScript, this would be an error – even though argIsString was assigned the value of a type guard, TypeScript simply lost that information. That’s unfortunate since we might want to re-use the same check in several places. To get around that, users often have to repeat themselves or use type assertions (casts).

In TypeScript 4.4, that is no longer the case. The above example works with no errors! When TypeScript sees that we are testing a constant value, it will do a little bit of extra work to see if it contains a type guard. If that type guard operates on a const, a readonly property, or an un-modified parameter, then TypeScript is able to narrow that value appropriately.

Different sorts of type guard conditions are preserved – not just typeof checks. For example, checks on discriminated unions work like a charm.

type Shape =
    | { kind: "circle", radius: number }
    | { kind: "square", sideLength: number };

function area(shape: Shape): number {
    const isCircle = shape.kind === "circle";
    if (isCircle) {
        // We know we have a circle here!
        return Math.PI * shape.radius ** 2;
    }
    else {
        // We know we're left with a square here!
        return shape.sideLength ** 2;
    }
}

As another example, here’s a function that checks whether two of its inputs have contents.

function doSomeChecks(
    inputA: string | undefined,
    inputB: string | undefined,
    shouldDoExtraWork: boolean,
) {
    let mustDoWork = inputA && inputB && shouldDoExtraWork;
    if (mustDoWork) {
        // Can access 'string' properties on both 'inputA' and 'inputB'!
        const upperA = inputA.toUpperCase();
        const upperB = inputB.toUpperCase();
        // ...
    }
}

TypeScript can understand that both inputA and inputB are both present if mustDoWork is true. That means we don’t have to write a non-null assertion like inputA! to convince TypeScript that inputA isn’t undefined.

One neat feature here is that this analysis works transitively. If we have a constant assigned to a condition which has more constants in it, and those constants are each assigned type guards, then TypeScript can propagate the conditions later on.

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

Note that there’s a cutoff – TypeScript doesn’t go arbitrarily deep when checking these conditions, but its analysis is deep enough for most checks.

This feature should make a lot of intuitive JavaScript code “just work” in TypeScript without it getting in your way. For more details, check out the implementation on GitHub!

Symbol and Template String Pattern Index Signatures

TypeScript lets us describe objects where every property has to have a certain type using index signatures. This allows us to use these objects as dictionary-like types, where we can use string keys to index into them with square brackets.

For example, we can write a type with an index signature that accepts string keys and maps to boolean values. If we try to assign anything other than a boolean value, we’ll get an error.

interface BooleanDictionary {
    [key: string]: boolean;
}

declare let myDict: BooleanDictionary;

// Valid to assign boolean values
myDict["foo"] = true;
myDict["bar"] = false;

// Error, "oops" isn't a boolean
myDict["baz"] = "oops";

While Map might be a better data structure here (specifically, a Map<string, boolean>), JavaScript objects are often more convenient to use or just happen to be what we’re given to work with.

Similarly, Array<T>s already defines a number index signature that lets us insert/retrieve values of type T.

// This is part of TypeScript's definition of the built-in Array type.
interface Array<T> {
    [index: number]: T;

    // ...
}

let arr = new Array<string>();

// Valid
arr[0] = "hello!";

// Error, expecting a 'string' value here
arr[1] = 123;

Index signatures are very useful to express lots of code out in the wild; however, until now they’ve been limited to string and number keys (and string index signatures have an intentional quirk where they can accept number keys since they’ll be coerced to strings anyway). That means that TypeScript didn’t allow indexing objects with symbol keys. TypeScript also couldn’t model an index signature of some subset of string keys – for example, an index signature which describes just properties whose names start with the text data-.

TypeScript 4.4 addresses these limitations, and allows index signatures for symbols and template string patterns.

For example, TypeScript now allows us to declare a type that can be keyed on arbitrary symbols.

interface Colors {
    [sym: symbol]: number;
}

const red = Symbol("red");
const green = Symbol("green");
const blue = Symbol("blue");

let colors: Colors = {};

colors[red] = 255;          // Assignment of a number is allowed
let redVal = colors[red];   // 'redVal' has the type 'number'

colors[blue] = "da ba dee"; // Error: Type 'string' is not assignable to type 'number'.

Similarly, we can write an index signature with template string pattern type. One use of this might be to exempt properties starting with data- from TypeScript’s excess property checking. When we pass an object literal to something with an expected type, TypeScript will look for excess properties that weren’t declared in the expected type.

interface Options {
    width?: number;
    height?: number;
}

let a: Options = {
    width: 100,
    height: 100,
    "data-blah": true, // Error! 'data-blah' wasn't declared in 'Options'.
};

interface OptionsWithDataProps extends Options {
    // Permit any property starting with 'data-'.
    [optName: `data-${string}`]: unknown;
}

let b: OptionsWithDataProps = {
    width: 100,
    height: 100,
    "data-blah": true,       // Works!

    "unknown-property": true,  // Error! 'unknown-property' wasn't declared in 'OptionsWithDataProps'.
};

A final note on index signatures is that they now permit union types, as long as they’re a union of infinite-domain primitive types – specifically:

  • string
  • number
  • symbol
  • template string patterns (e.g. `hello-${string}`)

An index signature whose argument is a union of these types will de-sugar into several different index signatures.

interface Data {
    [optName: string | symbol]: any;
}

// Equivalent to

interface Data {
    [optName: string]: any;
    [optName: symbol]: any;
}

For more details, read up on the pull request

Defaulting to the unknown Type in Catch Variables (--useUnknownInCatchVariables)

In JavaScript, any type of value can be thrown with throw and caught in a catch clause. Because of this, TypeScript historically typed catch clause variables as any, and would not allow any other type annotation:

try {
    // Who knows what this might throw...
    executeSomeThirdPartyCode();
}
catch (err) { // err: any
    console.error(err.message); // Allowed, because 'any'
    err.thisWillProbablyFail(); // Allowed, because 'any' :(
}

Once TypeScript added the unknown type, it became clear that unknown was a better choice than any in catch clause variables for users who want the highest degree of correctness and type-safety, since it narrows better and forces us to test against arbitrary values. Eventually TypeScript 4.0 allowed users to specify an explicit type annotation of unknown (or any) on each catch clause variable so that we could opt into stricter types on a case-by-case basis; however, for some, manually specifying : unknown on every catch clause was a chore.

That’s why TypeScript 4.4 introduces a new flag called --useUnknownInCatchVariables. This flag changes the default type of catch clause variables from any to unknown.

try {
    executeSomeThirdPartyCode();
}
catch (err) { // err: unknown

    // Error! Property 'message' does not exist on type 'unknown'.
    console.error(err.message);

    // Works! We can narrow 'err' from 'unknown' to 'Error'.
    if (err instanceof Error) {
        console.error(err.message);
    }
}

This flag is enabled under the --strict family of options. That means that if you check your code using --strict, this option will automatically be turned on. You may end up with errors in TypeScript 4.4 such as

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

In cases where we don’t want to deal with an unknown variable in a catch clause, we can always add an explicit : any annotation so that we can opt out of stricter types.

try {
    executeSomeThirdPartyCode();
}
catch (err: any) {
    console.error(err.message); // Works again!

}

For more information, take a look at the implementing pull request.

Exact Optional Property Types (--exactOptionalPropertyTypes)

In JavaScript, reading a missing property on an object produces the value undefined. It’s also possible to have an actual property with the value undefined. A lot of code in JavaScript tends to treat these situations the same way, and so initially TypeScript just interpreted every optional property as if a user had written undefined in the type. For example,

interface Person {
    name: string,
    age?: number;
}

was considered equivalent to

interface Person {
    name: string,
    age?: number | undefined;
}

What this meant is that a user could explicitly write undefined in place of age.

const p: Person = {
    name: "Daniel",
    age: undefined, // This is okay by default.
};

So by default, TypeScript doesn’t distinguish between a present property with the value undefined and a missing property. While this works most of the time, not all code in JavaScript makes the same assumptions. Functions and operators like Object.assignObject.keys, object spread ({ ...obj }), and forin loops behave differently depending on whether or not a property actually exists on an object. In the case of our Person example, this could potentially lead to runtime errors if the age property was observed in a context where its presence was important.

In TypeScript 4.4, the new flag --exactOptionalPropertyTypes specifies that optional property types should be interpreted exactly as written, meaning that | undefined is not added to the type:

// With 'exactOptionalPropertyTypes' on:
const p: Person = {
    name: "Daniel",
    age: undefined, // Error! undefined isn't a number
};

This flag is not part of the --strict family and needs to be turned on explicitly if you’d like this behavior. It also requires --strictNullChecks to be enabled as well. We’ll be making updates to DefinitelyTyped and other definitions to try to make the transition as straightforward as possible, but you may encounter some friction with this depending on how your code is structured.

For more information, you can take a look at the implementing pull request here.

tsc --help Updates and Improvements

TypeScript’s --help option has gotten a refresh! Thanks to work in part by Song Gao, we’ve brought in changes to update the descriptions of our compiler options and restyle the --help menu with some colors and other visual separation. While we’re still iterating a bit on our styles to work well across platform default themes, you can get an idea of what it looks like by taking a look at the original proposal thread.

Performance Improvements

Faster Declaration Emit

TypeScript now caches whether internal symbols are accessible in different contexts, along with how specific types should be printed. These changes can improve TypeScript’s general performance in code with fairly complex types, and is especially observed when emitting .d.ts files under the --declaration flag.

See more details here.

Faster Path Normalization

TypeScript often has to do several types of “normalization” on file paths to get them into a consistent format that the compiler can use everywhere. This involves things like replacing backslashes with slashes, or removing intermediate /./ and /../ segments of paths. When TypeScript has to operates over millions of these paths, these operations end up being a bit slow. In TypeScript 4.4, paths first undergo quick checks to see whether they need any normalization in the first place. These improvements together reduce project load time by 5-10% on bigger projects, and significantly more in massive projects that we’ve tested internally.

For more details, you can view the PR for path segment normalization along with the PR for slash normalization.

Faster Path Mapping

TypeScript now caches the way it constructs path-mappings (using the paths option in tsconfig.json). For projects with several hundred mappings, the reduction is significant. You can see more on the change itself.

Faster Incremental Builds with --strict

In what was effectively a bug, TypeScript would end up redoing type-checking work under --incremental compilations if --strict was on. This led to many builds being just as slow as if --incremental was turned off. TypeScript 4.4 fixes this, though the change has also been back-ported to TypeScript 4.3.

See more here.

Faster Source Map Generation for Big Outputs

TypeScript 4.4 adds an optimization for source map generation on extremely large output files. When building an older version of the TypeScript compiler, this results in around an 8% reduction in emit time.

We’d like to extend our thanks to David Michon who provided a simple and clean change to enable this performance win.

Faster --force Builds

When using --build mode on project references, TypeScript has to perform up-to-date checks to determine which files need to be rebuilt.When performing a --force build, however, that information is irrelevant since every project dependency will be rebuilt from scratch.In TypeScript 4.4, --force builds avoid those unnecessary steps and start a full build.See more about the change here.

https://github.com/microsoft/TypeScript/pull/43666

Spelling Suggestions for JavaScript

TypeScript powers the JavaScript editing experience in editors like Visual Studio and Visual Studio Code. Most of the time, TypeScript tries to stay out of the way in JavaScript files; however, TypeScript often has a lot of information to make confident suggestions, and ways of surfacing suggestions that aren’t too invasive.

That’s why TypeScript now issues spelling suggestions in plain JavaScript files – ones without // @ts-check or in a project with checkJs turned off. These are the same “Did you mean…?” suggestions that TypeScript files already have, and now they’re available in all JavaScript files in some form.

These spelling suggestions can provide a subtle clue that your code is wrong. We managed to find a few bugs in existing code while testing this feature!

For more details on this new feature, take a look at the pull request!

Inlay Hints

TypeScript is experimenting with editor support for inlay text which can help display useful information like parameter names inline in your code. You can think of it as a sort of friendly “ghost text”.

A preview of inlay hints in Visual Studio Code

This feature was built by Wenlu Wang whose pull request has more details. You can keep track of our progress integrating the feature with Visual Studio Code here.

Breaking Changes

lib.d.ts Changes for TypeScript 4.4

As with every TypeScript version, declarations for lib.d.ts (especially the declarations generated for web contexts), have changed. You can consult our list of known lib.dom.d.ts changes to understand what is impacted.

Using unknown in Catch Variables

Technically, users running with the --strict flag may see new errors around catch variables being unknown, especially if the existing code assumes only Error values have been caught. This often results in error messages such as:

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

To get around this, you can specifically add runtime checks to ensure that the thrown type matches your expected type. Otherwise, you can just use a type assertion, add an explicit : any to your catch variable, or turn off --useUnknownInCatchVariables.

Abstract Properties Do Not Allow Initializers

The following code is now an error because abstract properties may not have initializers:

abstract class C {
    abstract prop = 1;
    //       ~~~~
    // Property 'prop' cannot have an initializer because it is marked abstract.
}

Instead, you may only specify a type for the property:

abstract class C {
    abstract prop: number;
}

What’s Next?

To help your team plan around trying TypeScript 4.4, you can read up on the 4.4 Iteration plan. We’re currently aiming for a release candidate in mid-August, and a stable release at the end of August 2021. Between now and our release candidate, our team will be hard at work addressing known issues and listening to your feedback, so download our beta release today and let us hear your thoughts!

Happy Hacking!

– Daniel Rosenwasser and the TypeScript Team

6 comments

Leave a comment

  • Vitaly Turovsky
    Vitaly Turovsky

    I personally think that the new support for `typeof this[classProp]` is also great. It would be good to describe a case with it and classes.

    That’s extremely cool, that TypeScript is removing more restrictions from its base. But I’m still missing a good performance when working with @material-ui/core package 🙁
    But, 1.5x speed improvement for intellisense on component props is a good step, thank you.

  • Bert Verhelst
    Bert Verhelst

    This is a great release. I could understand what each feature does and have run into many of these limitations before. Can’t wait to try this out.

    The only limitation I still run into frequently is not being able to foreach over the values of an enum. Which forces me to also provide an array of strings next to the enum definition or replace the enum with an actual object.

    • Nickolay Platonov
      Nickolay Platonov

      Congrats with the new release! The typeguards limitation was a bit annoying indeed.

      Any hope of making the housekeeping round in the release cycle? There’s plenty of well-known long-standing problems, which are much more annoying than the typeguards.

      For example the declaration files limitations: https://github.com/microsoft/TypeScript/issues/35822
      Basically if you are using the class expressions in your code (aka mixins) – you are treated by the TS compiler as a 2nd class citizen.

      The thing is, there is a PR that provides the key piece to fix this problem, open from Nov 19, 2020: https://github.com/microsoft/TypeScript/pull/41587
      Its a “green” PR, passes all tests.

    • Mina Luke
      Mina Luke

      Do you mean converting the values of Enums to a union string literal? if yes, then that is available since TS v4.1

      Enum WeekDay {
        Monday: 'MON';
        Tuesday: 'TUE';
      }
      type Keys = keyof typeof WeekDay;  => equals 'Monday' | 'Tuesday'
      type Values = `${WeekDay}`;  => equals 'MON' | 'TUE'
      
  • Caio Abe
    Caio Abe

    Both `Control Flow Analysis of Aliased Conditions` and `Symbol and Template String Pattern Index Signatures` are wow new features. Not only having to deal with the assertions workaround have been a real hassle but also leveraging the quality of code of those whom are new to TS is another struggle in every team I worked to. Now it really seems that it “just works” as mentioned. Can’t wait for the future of TS.

    Thank you!

  • Simon Weaver
    Simon Weaver

    Some very cool new feature 🙂

    Noticed something weird with `delete` and `exactOptionalPropertyTypes`. What’s going on?

    interface Person {
        name: string,
        age?: number;
    }
    
    const person: Person = {
        name: "Daniel",
        age: 27
    };
    
    delete person.age;  // The operand of a 'delete' operator must be optional.

    PS. It does the same if you use let 😉

    Would also like to suggest adding a note to the docs about `delete` to remind people of how to delete properties besides setting them undefined.