Announcing TypeScript 2.9 RC

Daniel Rosenwasser

Today we’re excited to announce and get some early feedback with TypeScript 2.9’s Release Candidate. To get started with the RC, you can access it through NuGet, or use npm with the following command:

npm install -g typescript@rc

You can also get editor support by

Let’s jump into some highlights of the Release Candidate!

Support for symbols and numeric literals in keyof and mapped object types

TypeScript’s keyof operator is a useful way to query the property names of an existing type.

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

// Equivalent to the type
//  "name" | "age"
type PersonPropertiesNames = keyof Person;

Unfortunately, because keyof predates TypeScript’s ability to reason about unique symbol types, keyof never recognized symbolic keys.

const baz = Symbol("baz");

interface Thing {
    foo: string;
    bar: number;
    [baz]: boolean; // this is a computed property type
}

// Error in TypeScript 2.8 and earlier!
// `typeof baz` isn't assignable to `"foo" | "bar"`
let x: keyof Thing = baz;

TypeScript 2.9 changes the behavior of keyof to factor in both unique symbols as well as numeric literal types. As such, the above example now compiles as expected. keyof Thing now boils down to the type "foo" | "bar" | typeof baz.

With this functionality, mapped object types like Partial, Required, or Readonly also recognize symbolic and numeric property keys, and no longer drop properties named by symbols:

type Partial<T> = {
    [K in keyof T]: T[K]
}

interface Thing {
    foo: string;
    bar: number;
    [baz]: boolean;
}

type PartialThing = Partial<Thing>;

// This now works correctly and is equivalent to
//
//   interface PartialThing {
//       foo?: string;
//       bar?: number;
//       [baz]?: boolean;
//   }

 

Unfortunately this is a breaking change for any usage where users believed that for any type T, keyof T would always be assignable to a string. Because symbol- and numeric-named properties invalidate this assumption, we expect some minor breaks which we believe to be easy to catch. In such cases, there are several possible workarounds.

If you have code that’s really meant to only operate on string properties, you can use Extract<keyof T, string> to restrict symbol and number inputs:

function useKey<T, K extends Extract<keyof T, string>>(obj: T, k: K) {
    let propName: string = k;
    // ...
}
If you have code that's more broadly applicable and can handle more than just strings, you should be able to substitute string with string | number | symbol, or use the built-in type alias PropertyKey.

function useKey<T, K extends keyof T>(obj: T, k: K) {
    let propName: string | number | symbol = k; 
    // ...
}

 

Alternatively, users can revert to the old behavior under the --keyofStringsOnly compiler flag, but this is meant to be used as a transitionary flag.

import() types

One long-running pain-point in TypeScript has been the inability to reference a type in another module, or the type of the module itself, without including an import at the top of the file.

In some cases, this is just a matter of convenience – you might not want to add an import at the top of your file just to describe a single type’s usage. For example, to reference the type of a module at an arbitrary location, here’s what you’d have to write before TypeScript 2.9:

import * as _foo from "foo";

export async function bar() {
    let foo: typeof _foo = await import("foo");
}

 

In other cases, there are simply things that users can’t achieve today – for example, referencing a type within a module in the global scope is impossible today. This is because a file with any imports or exports is considered a module, so adding an import for a type in a global script file will automatically turn that file into a module, which drastically changes things like scoping rules and strict module within that file.

That’s why TypeScript 2.9 is introducing the new import(...) type syntax. Much like ECMAScript’s proposed import(...) expressions, import types use the same syntax, and provide a convenient way to reference the type of a module, or the types which a module contains.

// foo.ts
export interface Person {
    name: string;
    age: number;
}

// bar.ts
export function greet(p: import("./foo").Person) {
    return `
        Hello, I'm ${p.name}, and I'm ${p.age} years old.
    `;
}

 

Notice we didn’t need to add a top-level import specify the type of p. We could also rewrite our example from above where we awkwardly needed to reference the type of a module:

export async function bar() {
    let foo: typeof import("./foo") = await import("./foo");
}

 

Of course, in this specific example foo could have been inferred, but this might be more useful with something like the TypeScript language server plugin API.

Breaking changes

keyof types include symbolic/numeric properties

As mentioned above, key queries/keyof types now include names that are symbols and numbers, which can break some code that assumes keyof T is assignable to string. Users can avoid this by using the --keyofStringsOnly compiler option:

// tsconfig.json
{
    "compilerOptions": {
        "keyofStringsOnly": true
    }
}

 

Trailing commas not allowed on rest parameters

#22262 This break was added for conformance with ECMAScript, as trailing commas are not allowed to follow rest parameters in the specification.

Unconstrained type parameters are no longer assignable to object in strictNullChecks

#24013 The following code now errors:

function f<T>(x: T) {
    const y: object | null | undefined = x;
}

 

Since generic type parameters can be substituted with any primitive type, this is a precaution TypeScript has added under strictNullChecks. To fix this, you can add a constraint on object:

// We can add an upper-bound constraint here.
//           vvvvvvvvvvvvvvv
function f<T extends object>(x: T) {
    const y: object | null | undefined = x;
}

 

never can no longer be iterated over

#22964

Values of type never can no longer be iterated over, which may catch a good class of bugs. Users can avoid this behavior by using a type assertion to cast to the type any (i.e. foo as any).

What’s next?

We try to keep our plans easily discoverable on the TypeScript roadmap for everything else that’s coming in 2.9 and beyond. TypeScript 2.9 proper should arrive towards the end of the month, but to make that successful, we need all the help we can get, so download the RC today and let us know what you think!

Feel free to drop us a line on GitHub if you run into any problems, and let others know how you feel about this RC on Twitter and in the comments below!

0 comments

Discussion is closed.

Feedback usabilla icon