Announcing TypeScript 3.1

Daniel Rosenwasser

Today we’re announcing the release of TypeScript 3.1!If you haven’t heard of TypeScript, it’s a language that builds on top of modern JavaScript and adds static type-checking. When you write TypeScript code, you can use a tool like the TypeScript compiler to remove type-specific constructs, and rewrite any newer ECMAScript code to something that older browsers & runtimes can understand. Additionally, using types means that your tools can analyze your code more easily, and provide you with a rock-solid editing experience giving you things like code-completion, go-to-definition, and quick fixes. On top of that, it’s all free, open-source, and cross-platform, so if you’re interested in learning more, check out our website.

If you just want to get started with the latest version of TypeScript, you can get it through NuGet or via npm by running

npm install -g typescript

You can also get editor support for

Other editors may have different update schedules, but should all have excellent TypeScript support soon as well.

Let’s take a look at what this release of TypeScript brings us. Feel free to use the list below to jump around this post a bit.

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(person: Person) {
    const result = {} as StringyPerson;
    for (const prop in person) {
        result[prop] = String(person[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>(obj: T) {
    const result = {} as Stringify<T>;
    for (const prop in obj) {
        result[prop] = String(obj[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.

While very general, you might note that this functionality means that TypeScript is now better-equipped to express functions similar to Promise.all. While that specific change hasn’t made its way into this release, it may be coming in the near future.

Easier 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" />

Version redirects for TypeScript via typesVersions

Many TypeScript and JavaScript users love to use the bleeding-edge features of the languages and tooling. While we love the exuberance that drives this, it can sometimes create a difficult situation for library maintainers where maintainers are forced to choose between supporting new TypeScript features and not breaking older versions of TypeScript.

As an example, if you maintain a library which uses the unknown type from TypeScript 3.0, any of your consumers using earlier versions will be broken. There unfortunately isn’t a way to provide types for pre-3.0 versions of TypeScript while also providing types for 3.0 and later.

That is, until now. When using Node module resolution in TypeScript 3.1, when TypeScript cracks open a package.json file to figure out which files it needs to read, it first looks at a new field called typesVersions. A package.json with a typesVersions field might look like this:

{
  "name": "package-name",
  "version": "1.0",
  "types": "./index.d.ts",
  "typesVersions": {
    ">=3.1": { "*": ["ts3.1/*"] }
  }
}

This package.json tells TypeScript to check whether the current version of TypeScript is running. If it’s 3.1 or later, it figures out the path you’ve imported relative to the package, and reads from the package’s ts3.1 folder. That’s what that { "*": ["ts3.1/*"] } means – if you’re familiar with path mapping today, it works exactly like that.

So in the above example, if we’re importing from "package-name", we’ll try to resolve from [...]/node_modules/package-name/ts3.1/index.d.ts (and other relevant paths) when running in TypeScript 3.1. If we import from package-name/foo, we’ll try to look for [...]/node_modules/package-name/ts3.1/foo.d.ts and [...]/node_modules/package-name/ts3.1/foo/index.d.ts.

What if we’re not running in TypeScript 3.1 in this example? Well, if none of the fields in typesVersions get matched, TypeScript falls back to the types field, so here TypeScript 3.0 and earlier will be redirected to [...]/node_modules/package-name/index.d.ts.

Matching behavior

The way that TypeScript decides on whether a version of the compiler & language matches is by using Node’s semver ranges.

Multiple fields

typesVersions can support multiple fields where each field name is specified by the range to match on.

{
  "name": "package-name",
  "version": "1.0",
  "types": "./index.d.ts",
  "typesVersions": {
    ">=3.2": { "*": ["ts3.2/*"] },
    ">=3.1": { "*": ["ts3.1/*"] }
  }
}

Since ranges have the potential to overlap, determining which redirect applies is order-specific. That means in the above example, even though both the >=3.2 and the >=3.1 matchers support TypeScript 3.2 and above, reversing the order could have totally different behavior, so the above sample would not be equivalent to the following.

{
  "name": "package-name",
  "version": "1.0",
  "types": "./index.d.ts",
  "typesVersions": {
    // NOTE: this won't work!
    ">=3.1": { "*": ["ts3.1/*"] },
    ">=3.2": { "*": ["ts3.2/*"] }
  }
}

Refactor from .then() to await

The editing experience we know and love for TypeScript is actually built directly on top of the compiler itself, and so our team is directly responsible for bringing you features like refactorings in tools like Visual Studio, VS Code, and others.

Thanks to a lot of fantastic work from Elizabeth Dinella who interned with us this past summer, TypeScript 3.1 now has a refactoring to convert functions that return promises constructed with chains of .then() and .catch() calls to async functions that leverage await. You can see this in action in the video below.

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.

What’s next

You can take a look at our roadmap for things we haven’t covered here, as well as to get a sense of what’s coming up for the next release of TypeScript.

As an example of something we haven’t covered much here, TypeScript 3.1 also brought a series of error message improvements which should give helpful tips, and which will do a better job of accurately explaining where things might’ve gone wrong when you hit an error. Additionally, as an example of something new in store, 3.2 will provide strictly-typed call/bind/apply on function types. So keep an eye on the roadmap to see how things develop.

As always, we hope this release continues to make you happier and more productive. If you have any feedback, feel free to file an issue or reach out over Twitter.

Thanks, and happy hacking!

1 comment

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

  • Samuel Bronson 0

    Er, exactly how can the version redirects be order-dependent when they’re given in an unordered data structure?

Feedback usabilla icon