Announcing TypeScript 2.4

Daniel Rosenwasser

Today we’re excited to announce the release of TypeScript 2.4!

If you haven’t yet heard of TypeScript, it’s a superset of JavaScript that brings static types and powerful tooling to JavaScript. These static types are entirely optional and get erased away – you can gradually introduce them to your existing JavaScript code, and get around to adding them when you really need. At the same time, you can use them aggressively to your advantage to catch painful bugs, focus on more important tests that don’t have to do with types, and get a complete editing experience. In the end, you can run TypeScript code through the compiler to get clean readable JavaScript. That includes ECMAScript 3, 5, 2015, and so on.

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

Visual Studio 2015 users (who have Update 3) will be able to get TypeScript by simply installing it from here. Visual Studio 2017 users using Update 2 will be able to ge TypeScript 2.4 from this installer.

Built-in support for 2.4 should be coming to other editors very soon, but you can configure Visual Studio Code and our Sublime Text plugin to pick up any other version you need.

While our What’s New in TypeScript page as well as our 2.4 RC blog post may be a little more in-depth, let’s go over what’s here in TypeScript 2.4.

Dynamic import() expressions

Dynamic import expressions are a new feature in ECMAScript that allows you to asynchronously request a module at any arbitrary point in your program. These modules come back as Promises of the module itself, and can be await-ed in an async function, or can be given a callback with .then.

What this means in short that you can conditionally and lazily import other modules and libraries to make your application more efficient and resource-conscious. For example, here’s an async function that only imports a utility library when it’s needed:

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

Many bundlers have support for automatically splitting output bundles (a.k.a. “code splitting”) based on these import()expressions, so consider using this new feature with the esnext module target. Note that this feature won’t work with the es2015 module target, since the feature is anticipated for ES2018 or later.

String enums

TypeScript has had string literal types for quite some time now, and enums since its release. Having had some time to see how these features were being used, we revisited enums for TypeScript 2.4 to see how they could work together. This release of TypeScript now allows enum members to contain string initializers.

enum Colors {
    Red = "RED",
    Green = "GREEN",
    Blue = "BLUE",
}

String enums have the benefit that they’re much easier to debug with, and can also describe existing systems that use strings. Like numeric enums and string literal types, these enums can be used as tags in discriminated unions as well.

enum ShapeKind {
    Circle = "circle",
    Square = "square"
}

interface Circle {
    kind: ShapeKind.Circle;
    radius: number;
}

interface Square {
    kind: ShapeKind.Square;
    sideLength: number;
}

type Shape = Circle | Square;

Improved checking for generics

TypeScript 2.4 has improvements in how types are inferred when generics come into play, as well as improved checking when relating two generic function types.

Return types as inference targets

One such improvement is that TypeScript now can let types flow through return types in some contexts. This means you can decide more freely where to put your types. For example:

function arrayMap<T, U>(f: (x: T) => U): (a: T[]) => U[] {
    return a => a.map(f);
}

const lengths: (a: string[]) => number[] = arrayMap(s => s.length);

it used to be the case that s would need to be explicitly annotated or its type would be inferred as {}. While lengths could be left unannotated in that case, it felt surprising to some users that information from that type wasn’t used to infer the type of s.

In TypeScript 2.4, the type system knows s is a string from the type of lengths, which could better fit your stylistic choices.

This also means that some errors will be caught, since TypeScript can find better candidates than the default {} type (which is often too permissive).

let x: Promise<string> = new Promise(resolve => {
    resolve(10);
    //      ~~ Now correctly errors!
});

Stricter checking for generic functions

TypeScript now tries to unify type parameters when comparing two single-signature types. As a result, you’ll get stricter checks when relating two generic signatures which may catch some bugs.

type A = <T, U>(x: T, y: U) => [T, U];
type B = <S>(x: S, y: S) => [S, S];

function f(a: A, b: B) {
    a = b;  // Error
    b = a;  // Ok
}

As a temporary workaround for any breakage, you may be able to suppress some of these errors using the new --noStrictGenericChecks flag.

Strict contravariance for callback parameters

TypeScript has always compared parameters in a bivariant way. There are a number of reasons for this, and for the most part it didn’t appear to be a major issue until we heard more from users about the adverse effects it had with Promises and Observables. Relating two Promises or Observables should use the type arguments in a strictly covariant manner – a Promise<T> can only be related to a Promise<U> if T is relatable to U. However, because of parameter bivariance, along with the structural nature of TypeScript, this was previously not the case.

TypeScript 2.4 now tightens up how it checks two function types by enforcing the correct directionality on callback parameter type checks. For example:

interface Mappable<T> {
    map<U>(f: (x: T) => U): Mappable<U>;
}

declare let a: Mappable<number>;
declare let b: Mappable<string | number>;

a = b; // should fail, now does.
b = a; // should succeed, continues to do so.

In other words, TypeScript now catches the above bug, and since Mappable is really just a simplified version of Promise or Observable, you’ll see similar behavior with them too.

Note that this may be a breaking change for some, but this more correct behavior will benefit the vast majority of users in the long run.

Stricter checks on “weak types”

TypeScript 2.4 introduces the concept of “weak types”. A weak type is any type that contains nothing but all-optional properties. 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. That includes primitives like number, string, and boolean.

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'.

This check also catches situations like classes that might forget to implement members of an interface:

interface Foo {
    someMethod?(): void;
    someOtherMethod?(arg: number): string;
}

// Error! Did 'Dog' really need to implement 'Foo'?
class Dog implements Foo {
    bark() {
        return "woof!";
    }
}

This change to the type system may introduce some breakages, but in our exploration of existing codebases, this new check primarily catches silent errors that users weren’t aware of.

If you really are sure that a value should be compatible with a weak type, consider the following options:

  1. Declare properties in the weak type that are always expected to be present.
  2. Add an index signature to the weak type (i.e. [propName: string]: {}).
  3. Use a type assertion (i.e. opts as Options).

In the case above where the class Dog tried to implement Foo, it’s possible that Foo was being used to ensure code was implemented correctly later on in a derived class. You can get around this by performing class-interface merging.

class Dog {
    bark() {
        return "woof!";
    }
}

interface Dog implements Foo {
    // 'Dog' now inherits all of the methods from 'Foo'.
}

Enjoy!

You can read up our full what’s new in TypeScript page on our wiki for some more details on this new release. To also see a full list of breaking changes, you can look at our breaking changes page as well.

Keep in mind that any sort of constructive feedback that you can give us is always appreciated, and used as the basis of every new version of TypeScript. Any issues you run into, or ideas that you think would be helpful for the greater TypeScript community can be filed on our GitHub issue tracker.

If you’re enjoying TypeScript 2.4, let us know on Twitter with the #iHeartTypeScript hashtag on Twitter.

Thanks for reading up on this release, and happy hacking!

0 comments

Discussion is closed.

Feedback usabilla icon