Announcing TypeScript 3.5 RC

Avatar

Daniel

Today we’re happy to announce the availability of our release candidate (RC) of TypeScript 3.5. Our hope is to collect feedback and early issues to ensure our final release is simple to pick up and use right away.

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

npm install -g typescript@rc

You can also get editor support by

Let’s explore what’s new in 3.5!

Speed improvements

TypeScript 3.5 introduces several optimizations around type-checking and incremental builds.

Type-checking speed-ups

Much of the expressivity of our type system comes with a cost – any more work that we expect the compiler to do translates to longer compile times. Unfortunately, as part of a bug fix in TypeScript 3.4 we accidentally introduced a regression that could lead to an explosion in how much work the type-checker did, and in turn, type-checking time. The most-impacted set of users were those using the styled-components library. This regression was serious not just because it led to much higher build times for TypeScript code, but because editor operations for both TypeScript and JavaScript users became unbearably slow.

Over this past release, we focused heavily on optimizing certain code paths and stripping down certain functionality to the point where TypeScript 3.5 is actually faster than TypeScript 3.3 for many incremental checks. Not only have compile times fallen compared to 3.4, but code completion and any other editor operations should be much snappier too.

If you haven’t upgraded to TypeScript 3.4 due to these regressions, we would value your feedback to see whether TypeScript 3.5 addresses your performance concerns!

--incremental improvements

TypeScript 3.4 introduced a new --incremental compiler option. This option saves a bunch of information to a .tsbuildinfo file that can be used to speed up subsequent calls to tsc.

TypeScript 3.5 includes several optimizations to caching how the state of the world was calculated – compiler settings, why files were looked up, where files were found, etc. In scenarios involving hundreds of projects using TypeScript’s project references in --build mode, we’ve found that the amount of time rebuilding can be reduced by as much as 68% compared to TypeScript 3.4!

For more details, you can see the pull requests to

The Omit helper type

Much of the time, we want to create an object that omits certain properties. It turns out that we can express types like that using TypeScript’s built-in Pick and Exclude helpers. For example, if we wanted to define a Person that has no location property, we could write the following:

type Person = {
    name: string;
    age: number;
    location: string;
};

type RemainingKeys = Exclude<keyof Person, "location">;

type QuantumPerson = Pick<Person, RemainingKeys>;

// equivalent to
type QuantumPerson = {
    name: string;
    age: number;
};

Here we “subtracted” "location" from the set of properties of Person using the Exclude helper type. We then picked them right off of Person using the Pick helper type.

It turns out this type of operation comes up frequently enough that users will write a helper type to do exactly this:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Instead of making everyone define their own version of Omit, TypeScript 3.5 will include its own in lib.d.ts which can be used anywhere. The compiler itself will use this Omit type to express types created through object rest destructuring declarations on generics.

For more details, see the pull request on GitHub to add Omit, as well as the change to use Omit for object rest.

Improved excess property checks in union types

TypeScript has a feature called excess property checking in object literals. This feature is meant to detect typos for when a type isn’t expecting a specific property.

type Style = {
    alignment: string,
    color?: string
};

const s: Style = {
    alignment: "center",
    colour: "grey"
//  ^^^^^^ error! 
};

In TypeScript 3.4 and earlier, certain excess properties were allowed in situations where they really shouldn’t have been. For instance, TypeScript 3.4 permitted the incorrect name property in the object literal even though its types don’t match between Point and Label.

type Point = {
    x: number;
    y: number;
};

type Label = {
    name: string;
};

const thing: Point | Label = {
    x: 0,
    y: 0,
    name: true // uh-oh!
};

Previously, a non-disciminated union wouldn’t have any excess property checking done on its members, and as a result, the incorrectly typed name property slipped by.

In TypeScript 3.5, the type-checker at least verifies that all the provided properties belong to some union member and have the appropriate type, meaning that the sample above correctly issues an error.

Note that partial overlap is still permitted as long as the property types are valid.

const pl: Point | Label = {
    x: 0,
    y: 0,
    name: "origin" // okay
};

The --allowUmdGlobalAccess flag

In TypeScript 3.5, you can now reference UMD global declarations like

export as namespace foo;

from anywhere – even modules – using the new --allowUmdGlobalAccess flag.

This feature might require some background if you’re not familiar with UMD globals in TypeScript. A while back, JavaScript libraries were often published as global variables with properties tacked on – you sort of hoped that nobody picked a library name that was identical to yours. Over time, authors of modern JavaScript libraries started publishing using module systems to prevent some of these issues. While module systems alleviated certain classes of issues, they did leave users who were used to using global variables out in the rain.

As a work-around, many libraries are authored in a way that define a global object if a module loader isn’t available at runtime. This is typically leveraged when users target a module format called “UMD”, and as such, TypeScript has a way to describe this pattern which we’ve called “UMD global namespaces”:

export as namespace preact;

Whenever you’re in a script file (a non-module file), you’ll be able to access one of these UMD globals.

So what’s the problem? Well, not all libraries conditionally set their global declarations. Some just always create a global in addition to registering with the module system. We decided to err on the more conservative side, and many of us felt that if a library could be imported, that was probably the the intent of the author.

In reality, we received a lot of feedback that users were writing modules where some libraries were consumed as globals, and others were consumed through imports. So in the interest of making those users’ lives easier, we’ve introduced the allowUmdGlobalAccess flag in TypeScript 3.5.

For more details, see the pull request on GitHub.

Smarter union type checking

When checking against union types, TypeScript typically compares each constituent type in isolation. For example, take the following code:

type S = { done: boolean, value: number }
type T =
    | { done: false, value: number }
    | { done: true, value: number };

declare let source: S;
declare let target: T;

target = source;

Assigning source to target involves checking whether the type of source is assignable to target. That in turn means that TypeScript needs to check whether S:

{ done: boolean, value: number }

is assignable to T:

{ done: false, value: number } | { done: true, value: number }

Prior to TypeScript 3.5, the check in this specific example would fail, because S isn’t assignable to { done: false, value: number } nor { done: true, value: number }. Why? Because the done property in S isn’t specific enough – it’s booleanwhereas each constituent of T has a done property that’s specifically true or false. That’s what we meant by each constituent type being checked in isolation: TypeScript doesn’t just union each property together and see if S is assignable to that. If it did, some bad code could get through like the following:

interface Foo {
    kind: "foo";
    value: string;
}

interface Bar {
    kind: "bar";
    value: number;
}

function doSomething(x: Foo | Bar) {
    if (x.kind === "foo") {
        x.value.toLowerCase();
    }
}

// uh-oh - luckily TypeScript errors here!
doSomething({
    kind: "foo",
    value: 123,
});

So clearly this behavior is good for some set of cases. Was TypeScript being helpful in the original example though? Not really. If you figure out the precise type of any possible value of S, you can actually see that it matches the types in Texactly.

That’s why in TypeScript 3.5, when assigning to types with discriminant properties like in T, the language actually will go further and decompose types like S into a union of every possible inhabitant type. In this case, since boolean is a union of true and false, S will be viewed as a union of { done: false, value: number } and { done: true, value: number }.

For more details, you can see the original pull request on GitHub.

Higher order type inference from generic constructors

In TypeScript 3.4, we improved inference for when generic functions that return functions like so:

function compose<T, U, V>(
    f: (x: T) => U, g: (y: U) => V): (x: T) => V {
    
    return x => g(f(x))
}

took other generic functions as arguments, like so:

function arrayify<T>(x: T): T[] {
    return [x];
}

type Box<U> = { value: U }
function boxify<U>(y: U): Box<U> {
    return { value: y };
}

let newFn = compose(arrayify, boxify);

Instead of a relatively useless type like (x: {}) => Box<{}[]>, which older versions of the language would infer, TypeScript 3.4’s inference allows newFn to be generic. Its new type is <T>(x: T) => Box<T[]>.

TypeScript 3.5 generalizes this behavior to work on constructor functions as well.

class Box<T> {
    kind: "box";
    value: T;
    constructor(value: T) {
        this.value = value;
    }
}

class Bag<U> {
    kind: "bag";
    value: U;
    constructor(value: U) {
        this.value = value;
    }
}


function composeCtor<T, U, V>(
    F: new (x: T) => U, G: new (y: U) => V): (x: T) => V {
    
    return x => new G(new F(x))
}

let f = composeCtor(Box, Bag); // has type '<T>(x: T) => Bag<Box<T>>'
let a = f(1024); // has type 'Bag<Box<number>>'

In addition to compositional patterns like the above, this new inference on generic constructors means that functions that operate on class components in certain UI libraries like React can more correctly operate on generic class components.

type ComponentClass<P> = new (props: P) => Component<P>;
declare class Component<P> {
    props: P;
    constructor(props: P);
}

declare function myHoc<P>(C: ComponentClass<P>): ComponentClass<P>;

type NestedProps<T> = { foo: number, stuff: T };

declare class GenericComponent<T> extends Component<NestedProps<T>> {
}

// type is 'new <T>(props: NestedProps<T>) => Component<NestedProps<T>>'
const GenericComponent2 = myHoc(GenericComponent);

To learn more, check out the original pull request on GitHub.

Breaking changes

Generic type parameters are implicitly constrained to unknown

In TypeScript 3.5, generic type parameters without an explicit constraint are now implicitly constrained to unknown, whereas previously the implicit constraint of type parameters was the empty object type {}.

In practice, {} and unknown are pretty similar, but there are a few key differences:

  • {} can be indexed with a string (k["foo"]), though this is an implicit any error under --noImplicitAny.
  • {} is assumed to not be null or undefined, whereas unknown is possibly one of those values.
  • {} is assignable to object, but unknown is not.

The decision to switch to unknown is rooted that it is more correct for unconstrained generics – there’s no telling how a generic type will be instantiated.

On the caller side, this typically means that assignment to object will fail, and methods on Object like toString, toLocaleString, valueOf, hasOwnProperty, isPrototypeOf, and propertyIsEnumerable will no longer be available.

function foo<T>(x: T): [T, string] {
    return [x, x.toString()]
    //           ~~~~~~~~ error! Property 'toString' does not exist on type 'T'.
}

As a workaround, you can add an explicit constraint of {} to a type parameter to get the old behavior.

//             vvvvvvvvvv
function foo<T extends {}>(x: T): [T, string] {
    return [x, x.toString()]
}

From the caller side, failed inferences for generic type arguments will result in unknown instead of {}.

function parse<T>(x: string): T {
    return JSON.parse(x);
}

// k has type 'unknown' - previously, it was '{}'.
const k = parse("...");

As a workaround, you can provide an explicit type argument:

// 'k' now has type '{}'
const k = parse<{}>("...");

{ [k: string]: unknown } is no longer a wildcard assignment target

The index signature { [s: string]: any } in TypeScript behaves specially: it’s a valid assignment target for any object type. This is a special rule, since types with index signatures don’t normally produce this behavior.

Since its introduction, the type unknown in an index signature behaved the same way:

let dict: { [s: string]: unknown };
// Was okay
dict = () => {};

In general this rule makes sense; the implied constraint of “all its properties are some subtype of unknown” is trivially true of any object type. However, in TypeScript 3.5, this special rule is removed for { [s: string]: unknown }.

This was a necessary change because of the change from {} to unknown when generic inference has no candidates. Consider this code:

declare function someFunc(): void;
declare function fn<T>(arg: { [k: string]: T }): void;
fn(someFunc);

In TypeScript 3.4, the following sequence occurred:

  • No candidates were found for T
  • T is selected to be {}
  • someFunc isn’t assignable to arg because there are no special rules allowing arbitrary assignment to { [k: string]: {} }
  • The call is correctly rejected

Due to changes around unconstrained type parameters falling back to unknown (see above), arg would have had the type { [k: string]: unknown }, which anything is assignable to, so the call would have incorrectly been allowed. That’s why TypeScript 3.5 removes the specialized assignability rule to permit assignment to { [k: string]: unknown }.

Note that fresh object literals are still exempt from this check.

const obj = { m: 10 }; 
// okay
const dict: { [s: string]: unknown } = obj;

Depending on the intended behavior of { [s: string]: unknown }, several alternatives are available:

  • { [s: string]: any }
  • { [s: string]: {} }
  • object
  • unknown
  • any

We recommend sketching out your desired use cases and seeing which one is the best option for your particular use case.

Improved excess property checks in union types

As mentioned above, TypeScript 3.5 is stricter about excess property checks on constituents of union types.

We have not witnessed examples where this checking hasn’t caught legitimate issues, but in a pinch, any of the workarounds to disable excess property checking will apply:

  • Add a type assertion onto the object (e.g. { myProp: SomeType } as ExpectedType)
  • Add an index signature to the expected type to signal that unspecified properties are expected (e.g. interface ExpectedType { myProp: SomeType; [prop: string]: unknown })

Fixes to unsound writes to indexed access types

TypeScript allows you to represent the operation of accessing a property of an object via the name of that property:

type A = {
    s: string;
    n: number;
};

function read<K extends keyof A>(arg: A, key: K): A[K] {
    return arg[key];
} 

const a: A = { s: "", n: 0 };
const x = read(a, "s"); // x: string

While commonly used for reading values from an object, you can also use this for writes:

function write<K extends keyof A>(arg: A, key: K, value: A[K]): void {
    arg[key] = value;
}

In TypeScript 3.4, the logic used to validate a write was much too permissive:

function write<K extends keyof A>(arg: A, key: K, value: A[K]): void {
    // ???
    arg[key] = "hello, world";
}
// Breaks the object by putting a string where a number should be
write(a, "n", "oops!");

In TypeScript 3.5, this logic is fixed and the above sample correctly issues an error.

Most instances of this error represent potential errors in the relevant code. If you are convinced that you are not dealing with an error, you can use a type assertion instead.

lib.d.ts includes the Omit helper type

TypeScript 3.5 includes a new Omit helper type. As a result, any global declarations of Omit included in your project will result in the following error message:

Duplicate identifier 'Omit'.

Two workarounds may be used here:

  1. Delete the duplicate declaration and use the one provided in lib.d.ts.
  2. Export the existing declaration from a module file or a namespace to avoid a global collision. Existing usages can use an import or explicit reference to your project’s old Omit type.

Object.keys rejects primitives in ES5

In ECMAScript 5 environments, Object.keys throws an exception if passed any non-object argument:

// Throws if run in an ES5 runtime
Object.keys(10);

In ECMAScript 2015, Object.keys returns [] if its argument is a primitive:

// [] in ES6 runtime
Object.keys(10);

This is a potential source of error that wasn’t previously identified. In TypeScript 3.5, if target (or equivalently lib) is ES5, calls to Object.keys must pass a valid object.

In general, errors here represent possible exceptions in your application and should be treated as such. If you happen to know through other means that a value is an object, a type assertion is appropriate:

function fn(arg: object | number, isArgActuallyObject: boolean) {
    if (isArgActuallyObject) {
        const k = Object.keys(arg as object);
    }
}

Note that this change interacts with the change in generic inference from {} to unknown, because {} is a valid objectwhereas unknown isn’t:

declare function fn<T>(): T;

// Was okay in TypeScript 3.4, errors in 3.5 under --target ES5
Object.keys(fn());

What’s next?

The final release of TypeScript 3.5 should be coming out at the end of the month. We encourage you to give the RC a try so we can ensure TypeScript 3.5 provides the ideal coding experience.

Happy hacking!

  • Daniel Rosenwasser and the TypeScript Team
Avatar
Daniel Rosenwasser

Program Manager, TypeScript

Follow Daniel   

3 Comments
Nicola Castellani
Nicola Castellani 2019-05-24 07:54:20
Hi Daniel, I have a question: If you try to handle an union type like this: color: string | null and you set the color value as "undefined", typescript will not return any type check error, is this right? I know undefined and null are the same thing, but not the same type, if you print undefined === null in console it will return false. You can take a look here: https://www.loom.com/share/a8a530afc255417182e00bcfb842d340 I know this could be a dumb question, but I want to learn more about those void types and how ts handle them. Thanks for your time!