Announcing TypeScript 3.1 RC

Daniel Rosenwasser

Today we’re happy to announce the availability of the release candidate (RC) of TypeScript 3.1. Our intent with the RC is to gather any and all feedback so that we can ensure our final release is as pleasant as possible.If you’d like to give it a shot now, you can get the RC through NuGet, or use npm with the following command:

npm install -g typescript@rc

You can also get editor support by

Let’s look at what’s coming in TypeScript 3.1!

Mappable tuple and array types

Mapping over values in a list is one of the most common patterns in programming. As an example, let’s take a look at the following JavaScript code:

function stringifyAll(...elements) {
    return elements.map(x => String(x));
}

The stringifyAll function takes any number of values, converts each element to a string, places each result in a new array, and returns that array. If we want to have the most general type for stringifyAll, we’d declare it as so:

declare function stringifyAll(...elements: unknown[]): Array<string>;

That basically says, “this thing takes any number of elements, and returns an array of strings”; however, we’ve lost a bit of information about elements in that transformation.

Specifically, the type system doesn’t remember the number of elements user passed in, so our output type doesn’t have a known length either. We can do something like that with overloads:

declare function stringifyAll(...elements: []): string[];
declare function stringifyAll(...elements: [unknown]): [string];
declare function stringifyAll(...elements: [unknown, unknown]): [string, string];
declare function stringifyAll(...elements: [unknown, unknown, unknown]): [string, string, string];
// ... etc

Ugh. And we didn’t even cover taking four elements yet. You end up special-casing all of these possible overloads, and you end up with what we like to call the “death by a thousand overloads” problem. Sure, we could use conditional types instead of overloads, but then you’d have a bunch of nested conditional types.

If only there was a way to uniformly map over each of the types here…

Well, TypeScript already has something that sort of does that. TypeScript has a concept called a mapped object type which can generate new types out of existing ones. For example, given the following Person type,

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

we might want to convert each property to a string as above:

interface StringyPerson {
    name: string;
    age: string;
    isHappy: string;
}

function stringifyPerson(p: Person) {
    const result = {} as StringyPerson;
    for (const prop in p) {
        result[prop] = String(p[prop]);
    }
    return result;
}

Though notice that stringifyPerson is pretty general. We can abstract the idea of Stringify-ing types using a mapped object type over the properties of any given type:

type Stringify<T> = {
    [K in keyof T]: string
};

For those unfamiliar, we read this as “for every property named K in T, produce a new property of that name with the type string.

and rewrite our function to use that:

function stringifyProps<T>(p: T) {
    const result = {} as Stringify<T>;
    for (const prop in p) {
        result[prop] = String(p[prop]);
    }
    return result;
}

stringifyProps({ hello: 100, world: true }); // has type `{ hello: string, world: string }`

Seems like we have what we want! However, if we tried changing the type of stringifyAll to return a Stringify:

declare function stringifyAll<T extends unknown[]>(...elements: T): Stringify<T>;

And then tried calling it on an array or tuple, we’d only get something that’s almost useful prior to TypeScript 3.1. Let’s give it a shot on an older version of TypeScript like 3.0:

let stringyCoordinates = stringifyAll(100, true);

// No errors!
let first: string = stringyCoordinates[0];
let second: string = stringyCoordinates[1];

Looks like our tuple indexes have been mapped correctly! Let’s check the grab the length now and make sure that’s right:

   let len: 2 = stringyCoordinates.length
//     ~~~
// Type 'string' is not assignable to type '2'.

Uh. string? Well, let’s try to iterate on our coordinates.

 stringyCoordinates.forEach(x => console.log(x));
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Cannot invoke an expression whose type lacks a call signature. Type 'String' has no compatible call signatures.

Huh? What’s causing this gross error message? Well our Stringify mapped type not only mapped our tuple members, it also mapped over the methods of Array, as well as the length property! So forEach and length both have the type string!

While technically consistent in behavior, the majority of our team felt that this use-case should just work. Rather than introduce a new concept for mapping over a tuple, mapped object types now just “do the right thing” when iterating over tuples and arrays. This means that if you’re already using existing mapped types like Partial or Required from lib.d.ts, they automatically work on tuples and arrays now.

Properties on function declarations

In JavaScript, functions are just objects. This means we can tack properties onto them as we please:

export function readFile(path) {
    // ...
}

readFile.async = function (path, callback) {
    // ...
}

TypeScript’s traditional approach to this has been an extremely versatile construct called namespaces (a.k.a. “internal modules” if you’re old enough to remember). In addition to organizing code, namespaces support the concept of value-merging, where you can add properties to classes and functions in a declarative way:

export function readFile() {
    // ...
}

export namespace readFile {
    export function async() {
        // ...
    }
}

While perhaps elegant for their time, the construct hasn’t aged well. ECMAScript modules have become the preferred format for organizing new code in the broader TypeScript & JavaScript community, and namespaces are TypeScript-specific. Additionally, namespaces don’t merge with var, let, or const declarations, so code like the following (which is motivated by defaultProps from React):

export const FooComponent => ({ name }) => (
    <div>Hello! I am {name}</div>
);

FooComponent.defaultProps = {
    name: "(anonymous)",
};

can’t even simply be converted to

export const FooComponent => ({ name }) => (
    <div>Hello! I am {name}</div>
);

// Doesn't work!
namespace FooComponent {
    export const defaultProps = {
        name: "(anonymous)",
    };
}

All of this collectively can be frustrating since it makes migrating to TypeScript harder.

Given all of this, we felt that it would be better to make TypeScript a bit “smarter” about these sorts of patterns. In TypeScript 3.1, for any function declaration or const declaration that’s initialized with a function, the type-checker will analyze the containing scope to track any added properties. That means that both of the examples – both our readFile as well as our FooComponent examples – work without modification in TypeScript 3.1!

As an added bonus, this functionality in conjunction with TypeScript 3.0’s support for JSX.LibraryManagedAttributes makes migrating an untyped React codebase to TypeScript significantly easier, since it understands which attributes are optional in the presence of defaultProps:

// TypeScript understands that both are valid:
<FooComponent />
<FooComponent name="Nathan" />

Breaking Changes

Our team always strives to avoid introducing breaking changes, but unfortunately there are some to be aware of for TypeScript 3.1.

Vendor-specific declarations removed

TypeScript 3.1 now generates parts of lib.d.ts (and other built-in declaration file libraries) using Web IDL files provided from the WHATWG DOM specification. While this means that lib.d.ts will be easier to keep up-to-date, many vendor-specific types have been removed. We’ve covered this in more detail on our wiki.

Differences in narrowing functions

Using the typeof foo === “function” type guard may provide different results when intersecting with relatively questionable union types composed of {}, Object, or unconstrained generics.

function foo(x: unknown | (() => string)) {
    if (typeof x === "function") {
        let a = x()
    }
}

You can read more on the breaking changes section of our wiki.

Going forward

We’re looking forward to hearing about your experience with the RC. As always, keep an eye on our roadmap to get the whole picture of the release as we stabilize. We expect to ship our final release in just a few weeks, so give it a shot now!

0 comments

Discussion is closed.

Feedback usabilla icon