Announcing TypeScript 2.7

Daniel Rosenwasser

Today we’re proud to announce the release of TypeScript 2.7!

If you’re not familiar with TypeScript, it’s a language that brings optional static types to JavaScript by building on JavaScript itself. Running TypeScript code through its compiler emits clean readable JavaScript that runs on any browser, and can also make bleeding-edge ECMAScript features you write work on older browsers. That means that you can take advantage of the design time tooling and safety of types (like code completion & navigation), while still leveraging the familiarity, community, and ubiquity of JavaScript.

But if you already know what TypeScript is and want to start using 2.7, go ahead and get it on NuGet or download it over npm:

npm install -g typescript

Visual Studio 2015 users (who have Update 3) can install TypeScript 2.7 from here, and Visual Studio 2017 users using version 15.2 or later will be able to get TypeScript by simply installing it from here. Sublime Text Plugin users can install the new version from Package Control.

TypeScript 2.7 will be available for Visual Studio Code very soon, but eager users can get it working pretty easily in the meantime.

We’ve got a lot in 2.7. While you can always take a look at the roadmap, we’ve put together a quick list for a bird’s eye view of this release:

We’ve also put together a breaking changes section towards the end of this post which existing users should be aware of.

So without further ado, let’s see what this release brings!

Stricter class property checks

TypeScript 2.7 introduces a new strictness flag named --strictPropertyInitialization!

This flag makes sure that each instance property of a class gets set in the constructor body, or by a property initializer. In a sense, it brings some of the definite assignment checks from variables to instance properties in classes. For example:

class C {
    foo: number;
    bar = "hello";
    baz: boolean;
//  ~~~
//  Error! Property 'baz' has no initializer and is not assigned directly in the constructor.
    constructor() {
        this.foo = 42;
    }
}

In the above, baz never gets set, and TypeScript reports an error. If we truly meant for baz to potentially be undefined, we should have declared it with the type boolean | undefined.

There are certain scenarios where properties might be initialized indirectly (perhaps by a helper method or dependency injection library). In those cases, you can convince the type system you know better by using the definite assignment assertions for your properties.

class C {
    foo!: number;
    // ^
    // Notice this exclamation point!
    // This is the "definite assignment assertion" modifier.
    constructor() {
        this.initialize();
    }

    initialize() {
        this.foo = 0;
    }
}

Keep in mind that --strictPropertyInitialization will be turned on along with other --strict mode flags, which can impact your project. You can set the strictPropertyInitialization setting to false in your tsconfig.json‘s compilerOptions, or --strictPropertyInitialization false on the command line to turn off this checking.

Definite assignment assertions

As expressive as we try to make the type system, we understand there are certain times you might know better than TypeScript.

As we mentioned above, definite assignment assertions are a new syntax you can use to convince TypeScript that a property will definitely get assigned. But in addition to working on class properties, TypeScript 2.7 also allows you to use this feature on variable declarations!

let x!: number[];
initialize();
x.push(4);

function initialize() {
    x = [0, 1, 2, 3];
}

If we hadn’t added an exclamation point/bang (!) after x, TypeScript would have reported that x was never initialized. This can be handy in deferred-initialization, or re-initialization scenarios.

Easier ECMAScript module interoperability

Before ECMAScript modules were standardized in ES2015, the JavaScript ecosystem had several different module formats that worked in different ways. Once the standard passed, the community was left with a question of how to best interoperate with existing “legacy” module formats.

TypeScript and Babel took different approaches, and even now, there really isn’t a locked down standard. The short story is that if you’ve used Babel, Webpack, or React Native, and expected different import behaviors than you were used to, we have a new compiler option for you called --esModuleInterop.

Babel and Webpack allow users to import these CommonJS modules as default imports, but also provide each property on the namespace import (unless the module was marked with an __esModule flag).

import _, { pick } from "lodash";

_.pick(...);
pick(...);

Because TypeScript’s behavior differs, we added the --allowSyntheticDefaultImports flag in TypeScript 1.8 to allow users to get this behavior for type-checking (but not emit).

In general, TypeScript’s view of CommonJS (and AMD) modules is that namespace imports always correspond to the shape of a CommonJS module object, and that a default import just corresponds to a member on that module named default. Under this assumption, you can create a named import

import { range } from "lodash";

for (let i of range(10)) {
    // ...
}

However, ES namespace imports aren’t callable, so this approach doesn’t always make sense.

import * as express from "express";

// Should be an error in any valid implementation.
let app = express();

To give users the same runtime behavior as Babel or Webpack, TypeScript provides a new --esModuleInterop flag when emitting to

legacy module formats.

Under the new --esModuleInterop flag, these callable CommonJS modules must be imported as default imports like so:

import express from "express";

let app = express();

We strongly suggest that Node.js users leverage this flag with a module target of commonjs for libraries like express, which export a callable/constructable module.

Webpack users may want to use this as well; however, your code should target esnext modules with a moduleResolution strategy of node. Using esnext modules with --esModuleInterop really only has the effect of turning on --allowSyntheticDefaultImports.

unique symbol types and const-named properties

TypeScript 2.7 understands ECMAScript symbols more deeply, allowing you to use them more flexibly.

One highly-demanded use-case is being able to declare well-typed properties with symbols. For an example, take the following:

const Foo = Symbol("Foo");
const Bar = Symbol("Bar");

let x = {
    [Foo]: 100,
    [Bar]: "hello",
};

let a = x[Foo]; // has type 'number'
let b = x[Bar]; // has type 'string'

As you can see, TypeScript can keep track of the fact that x has properties declared using the symbols Foo and Bar since both Foo and Bar were declared as constants. TypeScript leverages this fact and gives both Foo and Bar a new kind of type: unique symbols.

unique symbols are subtype of symbol, and are produced only from calling Symbol() or Symbol.for(), or from explicit type annotations. They can only occur on const declarations and readonly static properties, and in order to reference an existing unique symbol type, you’ll have to use the typeof operator. Each reference to a unique symbol implies a completely unique identity that’s tied to a given declaration.

// Works
declare const Foo: unique symbol;

// Error! 'Bar' isn't a constant.
let Bar: unique symbol = Symbol();

// Works - refers to a unique symbol, but its identity is tied to 'Foo'.
let Baz: typeof Foo = Foo;

// Also works.
class C {
    static readonly StaticSymbol: unique symbol = Symbol();
}

Because each unique symbol has a completely separate identity, no two unique symbol types are assignable or comparable to each other.

const Foo = Symbol();
const Bar = Symbol();

// Error: can't compare two unique symbols.
if (Foo === Bar) {
    // ...
}

Other potential use-cases include using symbols for tagged unions.

// ./ShapeKind.ts
export const Circle = Symbol("circle");
export const Square = Symbol("square");

// ./ShapeFun.ts
import * as ShapeKind from "./ShapeKind";

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

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

function area(shape: Circle | Square) {
    if (shape.kind === ShapeKind.Circle) {
        // 'shape' has type 'Circle'
        return Math.PI * shape.radius ** 2;
    }
    // 'shape' has type 'Square'
    return shape.sideLength ** 2;
}

Cleaner output in --watch mode

TypeScript’s --watch mode now clears the screen after a re-compilation is requested. This can make it much easier to read messages from the current compilation. We’d like to thank both Philipp Kretzschmar and Joshua Goldberg for helping out on this feature!

Prettier --pretty output

TypeScript’s --pretty flag can make error messages easier to read and manage. We have two main improvements in this functionality. First, thanks to a pull request from Joshua Goldberg --pretty now uses colors for file names, diagnostic codes, and line numbers. Second, thanks to a separate pull request by Orta Therox, file names and positions are formatted in such a way that common terminals (including the one embedded in Visual Studio Code) can allow a Ctrl+Click, Cmd+Click, Alt+Click, etc. to jump to the appropriate location in your editor

Numeric Separators

TypeScript 2.7 introduces support for ECMAScript’s numeric separators proposal. This feature allows users to place underscores (_) in between digits to visually distinguish groups of digits (much like how commas and periods are often used to group numbers).

// Constants
const COULOMB = 8.957_551_787e9; // N-m^2 / C^2
const PLANCK = 6.626_070_040e-34; // J-s
const JENNY = 867_5309; // C-A-L^2

These separators are also useful for binary and hexadecimal numbers.

let bits = 0b0010_1010;
let routine = 0xC0FFEE_F00D_BED;
let martin = 0xF0_1E_

Note that, perhaps counterintuitively, numbers in JavaScript are not well-suited to represent credit card and telephone numbers. Strings will act as better representations in such cases.

Fixed Length Tuples

In TypeScript 2.6 and earlier, [number, string, string] was considered a subtype of [number, string]. This was motivated by TypeScript’s structural nature; the first and second elements of a [number, string, string] are respectively subtypes of the first and second elements of [number, string], and the “trailing” string type is assignable to the union of element types from [number, string]. However, after examining real world usage of tuples, we noticed that most situations in which this was permitted was typically undesirable.

Thanks to a pull request from Kiara Grouwstra, tuple types now encode their arity into the type of their respective length property, and tuples of different arities are no longer assignable to each other. This is accomplished by leveraging numeric literal types, which now allow tuples to be distinct from tuples of different arities.

Conceptually, you might consider the type [number, string] to be equivalent to the following declaration of NumStrTuple:

interface NumStrTuple extends Array<number | string> {
    0: number;
    1: string;
    length: 2; // using the numeric literal type '2'
}

Note that this is a breaking change. If you need to resort to the original behavior in which tuples only enforce a minimum size, you can use a similar declaration that does not explicitly define a length property, falling back to number.

interface MinimumNumStrTuple extends Array<number | string> {
    0: number;
    1: string;
}

in operator narrowing and accurate instanceof

TypeScript 2.7 brings two new changes to type narrowing – the ability to get a more specific type for a value by running certain types of checks called “type guards”.

First, the instanceof operator is now leverages the inheritance chain instead of

relying on structural compatibility, more accurately reflecting whether how instanceof may behave at runtime. This can help avoid certain complex issues when instanceof narrows from structurally similar (but unrelated) types.

Second, thanks to GitHub user IdeaHunter, the in operator now acts as a type guard, narrowing out types that don’t explicitly declare properties of a given name.

interface A { a: number };
interface B { b: string };

function foo(x: A | B) {
    if ("a" in x) {
        return x.a;
    }
    return x.b;
}

Smarter object literal inference

There are certain patterns in JavaScript where users will omit properties so that all uses of those properties are effectively undefined.

let foo = someTest ? { value: 42 } : {};

TypeScript used seek the best common supertype between { value: number } and {}, ending up with {}. While being technically correct, this wasn’t very useful.

Starting with version 2.7, TypeScript now “normalizes” each object literal’s type to account for every property, inserting an optional property of type undefined on each object type, and unioning them together.

From the above example, the new type of foo would be { value: number } | { value?: undefined }. Combined with the ways TypeScript can narrow, this lets us write expressive code that TypeScript can still understand. As another example, take the following:

// Has type
//  | { a: boolean, aData: number, b?: undefined }
//  | { b: boolean, bData: string, a?: undefined }
let bar = Math.random() < 0.5 ?
    { a: true, aData: 100 } :
    { b: true, bData: "hello" };

if (bar.b) {
    // TypeScript now knows that 'bar' has the type
    //
    //   '{ b: boolean, bData: string, a?: undefined }'
    //
    // so it knows that 'bData' is available.
    bar.bData.toLowerCase()
}

Here, TypeScript is able to narrow the type of bar based on checking its b property, allowing us to access the bData property.

Breaking Changes

This release brings some minor breaking changes:

  • Tuples now have fixed numeric length properties.
  • instanceof and in now have slightly different narrowing behavior.
  • Inferences from generic signatures now use base constraint types of type parameters instead of any.
  • The setSelectionRange API now only accepts "forward" | "backward" | "none".
  • allowSyntheticDefaultImports no longer synthesizes default imports from TypeScript implementation files (i.e. .ts and .tsx).

Additionally, as mentioned above, users with the --strict setting on will automatically be opted in to --strictPropertyInitialization which errors on properties which are not directly initialized on their declarations or in constructor bodies. While easy to opt out of by explicitly turning this check off, your code may be impacted.

You can get a detailed look from our list of breaking changes issues for TypeScript 2.7 on GitHub, or keep track of general breaking changes on our Breaking Changes wiki page.

What’s next?

We always try to bring more joy to the TypeScript experience. We hope this release continues the tradition of making you more productive and expressive to bring that joy to the core experience.

Now that 2.7 is out, we’ve got some fantastic things in mind for TypeScript 2.8, including conditional types! While plans are still currently in flux, you can keep a close watch on our roadmap to get an idea of what’s on the TypeScript horizon.

Let us know what you think of this release over on Twitter or in the comments below, and feel free to report issues and suggestions filing a GitHub issue.

Happy Hacking!

0 comments

Discussion is closed.

Feedback usabilla icon