Announcing TypeScript 2.4 RC

Daniel Rosenwasser

Today we’re releasing our RC of TypeScript 2.4. To get started with the latest stable version of TypeScript, you can grab it through NuGet, or use the following command with npm:

npm install -g typescript@rc

Visual Studio 2015 users (who have Update 3) can install TypeScript from here, and Visual Studio 2017 users using Update 2 will be able to get TypeScript by simply installing it from here.

To get it working elsewhere, you can easily configure Visual Studio Code and our Sublime Text plugin to pick up whatever version you need. Other editors may have different approaches to swapping TypeScript in.

Dynamic Import Expressions

TypeScript 2.4 is bringing support for ECMAScript’s new import() calls. These calls import a module and return a promise to that module.

For example, imagine a webpage that allows you to create and edit images. When you’re working with one file, the page will allow you to download that file immediately; but if you’re working on multiple images, you can save all of them as a .zip file.

You might have a utility library to create a zip file, but since downloading multiple files isn’t that common, you’d want to load that functionality lazily. import() expressions let you load a module on the fly as a Promise like so:

async function getZipFile(name: string, files: File[]): Promise<File> {
    const zipUtil = await import('./utils/create-zip-file');
    const zipContents = await zipUtil.getContentAsBlob(files);
    return new File(zipContents, name);
}

This feature is so incredibly useful in situations where you want to conditionally import modules. Projects that use bundlers like Webpack can operate on these import() calls and split code into smaller bundles that can be lazily loaded.

What all of this means in the end is

  • you can send less JS over the wire for more common scenarios
  • your users can get faster page load times for critical content

Safer callback parameter checking

When checking whether two functions are assignable to one another, TypeScript checks whether their parameters are bidirectionally assignable. We call this function parameter bivariance. There are a number of reasons for this, but it mainly stems from TypeScript’s structural nature and how we’ve tried to match people’s intuition.

Our experience has been that users generally don’t run into issues with this all that much; however, we did start to see that this model broke down on containers which handed internal data off through callbacks – specifically, Promises. For example, a Promise<Animal> was assignable to Promise<Dog>, which is incorrect. You can see similar behavior in the following sample:

interface Animal { animalStuff: any }
interface Dog extends Animal { bark(): void }

interface BasicCollection<T> {
    forEach(callback: (value: T) => void): void;
}

declare let animalCollection: BasicCollection<Animal>;
declare let dogCollection: BasicCollection<Dog>;

// This should be an error, but TypeScript 2.3 and below allow it.
dogCollection = animalCollection;

To solve this issue, TypeScript 2.4 now tightens things up and compares the parameters which are callbacks specially. When checking callbacks, TypeScript will be strict about checking parameters contravariantly with respect to the current check. Parameter bivariance still applies otherwise, but we found this to be an effective safety check without drastically changing the type system.

Since this may be a bit technical, you can read more on the original pull request. The short story is that you’ll see much better checking when using Promises, Observables, and anywhere else you’ve been using callbacks.

Be aware though – this new check can introduce new type-checking errors in existing codebases. For more details, see our Breaking Changes section.

Weak types

Back in TypeScript 1.6, we added a check for excess properties in object literals. This check looked for unexpected properties in object literals, and it happened to catch a large class of bugs. The only shortcoming of the check was that if you didn’t immediately pass your object literal to something of the appropriate type, the check wouldn’t be triggered.

In TypeScript 2.4, we’re adding a similar check for what we call weak types. Any type that contains only optional properties is considered a weak type since it provides few restrictions on what can be assigned to it. For example, this Options type is a weak type:

interface Options {
    data?: string,
    timeout?: number,
    maxRetries?: number,
}

In TypeScript 2.4, it’s now an error to assign anything to a weak type when there’s no overlap in properties. For example:

function sendMessage(options: Options) {
    // ...
}

const opts = {
    payload: "hello world!",
    retryOnFail: true,
}

// Error!
sendMessage(opts);
// No overlap between the type of 'opts' and 'Options' itself.
// Maybe we meant to use 'data'/'maxRetries' instead of 'payload'/'retryOnFail'.

You can think of this as TypeScript “toughening up” the weak guarantees of these types to catch what would otherwise be silent bugs.

Since this is a breaking change, you may need to know about the workarounds which are the same as those for strict object literal checks:

  1. Declare the properties if they really do exist.
  2. Add an index signature to the weak type (i.e. [propName: string]: {}).
  3. Use a type assertion (i.e. opts as Options).

String enums

Enums were an early feature TypeScript provided that have been handy for marking a set of well-known related numeric values.

enum E {
    A = 1,
    B = 2,
    C = 3,
}

In TypeScript 1.8, we also released string literal types which could be used in a union.

function setStatus(status: "ready" | "running" | "finished") {
    // ...
}

setStatus("ready");

setStatus("stopped");
//        ~~~~~~~~~
// Error: Type
//
//   '"stopped"'
//
// is not assignable to type
//
//   '"ready" | "running" | "finished"'.

There were benefits to both constructs, but each group of users wanted some functionality from the other.

For example, string literal types serialize very well over transfer protocols and during debugging – after all, they’re just strings. But this isn’t the case for numeric enums, which end up as plain numbers without obvious meaning.

But string literal unions can be cumbersome for more advanced scenarios. If you want a well-known set of constants for each string, then you typically have to declare them yourself.

That’s why for TypeScript 2.4, we’re introducing string enums!

enum ActionType {
    AddUser = "ADD_USER",
    DeleteUser = "DELETE_USER",
    RenameUser = "RENAME_USER",

    // Aliases
    RemoveUser = DeleteUser,
}

String enums have a simple representation, but come with the caveat that they don’t create a reverse-mapping. In other words, you can’t index into ActionType with the string "ADD_USER" (by writing ActionType["ADD_USER"]) to get the name of AddUser.

Have fun!

You can see the full list of what’s new in 2.4 here on our wiki.

Then give TypeScript 2.4 RC a try and let us know what you think! If you’re having an awesome experience, use the #iHeartTypeScript hashtag on Twitter and let us know.

If you run into any places you think we could improve, we’re always all-ears on suggestions.

0 comments

Discussion is closed.

Feedback usabilla icon