January 21st, 2022

Announcing TypeScript 4.6 Beta

Daniel Rosenwasser
Principal Product Manager

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

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

Here’s a quick list of what’s new in TypeScript 4.6!

Allowing Code in Constructors Before super()

In JavaScript classes it’s mandatory to call super() before referring to this. TypeScript enforces this as well, though it was a bit too strict in how it ensured this. In TypeScript, it was previously an error to contain any code at the beginning of a constructor if its containing class had any property initializers.

class Base {
    // ...
}

class Derived extends Base {
    someProperty = true;

    constructor() {
        // error!
        // have to call 'super()' first because it needs to initialize 'someProperty'.
        doSomeStuff();
        super();
    }
}

This made it cheap to check that super() got called before this was referenced, but this ends up rejecting a lot of valid code. TypeScript 4.6 is now much more lenient in that check and permits other code to run before super()., all while still ensuring that super() occurs at the top-level before any references to this.

We’d like to extend our thanks to Joshua Goldberg for patiently working with us to land this change!

Improved Recursion Depth Checks

TypeScript has some interesting challenges due to the fact that it’s built on a structural type system that also provides generics.

In a structural type system, object types are compatible based on the members they have.

interface Source {
    prop: string;
}

interface Target {
    prop: number;
}

function check(source: Source, target: Target) {
    target = source;
    // error!
    // Type 'Source' is not assignable to type 'Target'.
    //   Types of property 'prop' are incompatible.
    //     Type 'string' is not assignable to type 'number'.
}

Notice that whether or not Source is compatible with Target has to do with whether their properties are assignable. In this case, that’s just prop.

When you introduce generics into this, there are some harder questions to answer. For instance, is a Source<string> assignable to a Target<number> in the following case?

interface Source<T> {
    prop: Source<Source<T>>;
}

interface Target<T> {
    prop: Target<Target<T>>;
}

function check(source: Source<string>, target: Target<number>) {
    target = source;
}

In order to answer that, TypeScript needs to check whether the types of prop are compatible. That leads to the another question: is a Source<Source<string>> assignable to a Target<Target<number>>? To answer that, TypeScript checks whether prop is compatible for those types, and ends up checking whether Source<Source<Source<string>>> is assignable to Target<Target<Target<number>>>. Keep going for a bit, and you might notice that the type infinitely expands the more you dig in.

TypeScript has a few heuristics here – if a type appears to be infinitely expanding after encountering a certain depth check, then it considers that the types could be compatible. This is usually enough, but embarrassingly there were some false-negatives that this wouldn’t catch.

interface Foo<T> {
    prop: T;
}

declare let x: Foo<Foo<Foo<Foo<Foo<Foo<string>>>>>>;
declare let y: Foo<Foo<Foo<Foo<Foo<string>>>>>;

x = y;

A human reader can see that x and y should be incompatible in the above example. While the types are deeply nested, that’s just a consequence of how they were declared. The heuristic was meant to capture cases where deeply-nested types were generated through exploring the types, not from when a developer wrote that type out themselves.

TypeScript 4.6 is now able to distinguish these cases, and correctly errors on the last example. Additionally, because the language is no longer concerned with false-positives from explicitly-written types, TypeScript can conclude that a type is infinitely expanding much earlier, and save a bunch of work in checking for type compatibility. As a result, libraries on DefinitelyTyped like redux-immutablereact-lazylog, and yup saw a 50% reduction in check-time.

You may already have this change because it was cherry-picked into TypeScript 4.5.3, but it is a notable feature of TypeScript 4.6 which you can read up more about here.

Indexed Access Inference Improvements

TypeScript now can correctly infer to indexed access types which immediately index into a mapped object type.

interface TypeMap {
    "number": number;
    "string": string;
    "boolean": boolean;
}

type UnionRecord<P extends keyof TypeMap> = { [K in P]:
    {
        kind: K;
        v: TypeMap[K];
        f: (p: TypeMap[K]) => void;
    }
}[P];

function processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) {
    record.f(record.v);
}

processRecord({
    kind: "string",
    v: "hello!",

    // 'val' used to implicitly have the type 'string | number | boolean',
    // but now is correctly inferred to just 'string'.
    f: val => {
        console.log(val.toUpperCase());
    }
})

This pattern was already supported and allowed TypeScript to understand that the call to record.f(record.v) is valid, but previously the call to processRecord would give poor inference results for val

TypeScript 4.6 improves this so that no type assertions are necessary within the call to processRecord.

For more information, you can read up on the pull request.

Control Flow Analysis for Dependent Parameters

A signature can be declared with a rest parameter whose type is a discriminated union of tuples.

function func(...args: ["str", string] | ["num", number]) {
    // ...
}

What this says is that when the first argument is the string "str", then its second argument is a string, and when its first argument is the string "num", its second argument is a number.

In cases where TypeScript infers from a signature with this kind of rest parameter, TypeScript can now narrow parameters that depend on others.

type Func = (...args: ["a", number] | ["b", string]) => void;

const f1: Func = (kind, payload) => {
    if (kind === "a") {
        payload.toFixed();  // 'payload' narrowed to 'number'
    }
    if (kind === "b") {
        payload.toUpperCase();  // 'payload' narrowed to 'string'
    }
};

f1("a", 42);
f1("b", "hello");

For more information, see the change on GitHub.

More Syntax and Binding Errors in JavaScript

TypeScript has expanded its set of syntax and binding errors in JavaScript files. You’ll see these new errors if you open JavaScript files in an editor like Visual Studio or Visual Studio Code, or if you run JavaScript code through the TypeScript compiler – even if you don’t turn on checkJs or add a // @ts-check comment to the top of your files.

As one example, if you have two declarations of a const in the same scope of a JavaScript file, TypeScript will now issue an error on those declarations.

const foo = 1234;
//    ~~~
// error: Cannot redeclare block-scoped variable 'foo'.

// ...

const foo = 5678;
//    ~~~
// error: Cannot redeclare block-scoped variable 'foo'.

As another example, TypeScript will let you know if a modifier is being incorrectly used.

function container() {
    export function foo() {
//  ~~~~~~
// error: Modifiers cannot appear here.

    }
}

These errors can be disabled by adding a // @ts-nocheck at the top of your file, but we’re interested in hearing some early feedback about how it works for your JavaScript workflow. You can easily try it out for Visual Studio Code by installing the TypeScript and JavaScript Nightly Extension, and read up more on the first and second pull requests.

TypeScript Trace Analyzer

Occasionally, teams may encounter types that are computationally expensive to create and compare. TypeScript has a --generateTrace flag to help identify some of those expensive types, or sometimes help diagnose issues in the TypeScript compiler. While the information generated by --generateTrace can be useful (especially with some information added in TypeScript 4.6), it can often be hard to read in existing trace visualizers.

We recently published a tool called @typescript/analyze-trace to get a more digestible view of this information. While we don’t expect everyone to need analyze-trace, we think it can come in handy for any team that is running into build performance issues with TypeScript.

For more information, see the analyze-trace tool’s repo.

Breaking Changes

Object Rests Drop Unspreadable Members from Generic Objects

Object rest expressions now drop members that appear to be unspreadable on generic objects. In the following example…

class Thing {
    someProperty = 42;

    someMethod() {
        // ...
    }
}

function foo<T extends Thing>(x: T) {
    let { someProperty, ...rest } = x;

    // Used to work, is now an error!
    // Property 'someMethod' does not exist on type 'Omit<T, "someProperty" | "someMethod">'.
    rest.someMethod();
}

the variable rest used to have the type Omit<T, "someProperty"> because TypeScript would strictly analyze which other properties were destructured. This doesn’t model how ...rest would work in a destructuring from a non-generic type because someMethod would typically be dropped as well. In TypeScript 4.6, the type of rest is Omit<T, "someProperty" | "someMethod">.

This can also come up in cases when destructuring from this. When destructuring this using a ...rest element, unspreadable and non-public members are now dropped, which is consistent with destructuring instances of a class in other places.

class Thing {
    someProperty = 42;

    someMethod() {
        // ...
    }

    someOtherMethod() {
        let { someProperty, ...rest } = this;

        // Used to work, is now an error!
        // Property 'someMethod' does not exist on type 'Omit<T, "someProperty" | "someMethod">'.
        rest.someMethod();
    }
}

For more details, see the corresponding change here.

JavaScript Files Always Receive Grammar and Binding Errors

Previously, TypeScript would ignore most grammar errors in JavaScript apart from accidentally using TypeScript syntax in a JavaScript file. TypeScript now shows JavaScript syntax and binding errors in your file, such as using incorrect modifiers, duplicate declarations, and more. These will typically be most apparent in Visual Studio Code or Visual Studio, but can also occur when running JavaScript code through the TypeScript compiler.

You can explicitly turn these errors off by inserting a // @ts-nocheck comment at the top of your file.

For more information, see the first and second implementing pull requests for these features.

What’s Next?

Over the next few weeks, we’ll be hard at work addressing any issues you encounter with the beta release, and bringing those changes in for a release candidate. For some details including target release dates, you can take a look at the iteration plan for TypeScript 4.6.

We hope you give this beta a shot and let us know what you think!

Happy Hacking!

– Daniel Rosenwasser and the TypeScript Team

Category
TypeScript

Author

Daniel Rosenwasser
Principal Product Manager

Daniel Rosenwasser is the product manager of the TypeScript team. He has a passion for programming languages, compilers, and great developer tooling.

2 comments

Discussion is closed. Login to edit/delete existing comments.

  • Wouter Kettlitz

    Thank you, typescript is my favorite language. Only problem i have is that errors are sometimes truncated and it seems like there is no way to see the whole thing. Atleast thats my problem in vscode

  • Calvin Evans

    All great changes as usual but I particularly appreciate the index access interface improvements. Relatively small little thing, but will make writing a certain class of functions far more ergonomic than it is currently.

    Thank you once again to everyone who continues to work so hard on these improvements.