Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Announcing TypeScript 3.0

Typescript 3.0 is here! Today marks a new milestone in the TypeScript journey, serving JavaScript users everywhere.

If you're unfamiliar with TypeScript, it's not too late to learn about it now! TypeScript is an extension of JavaScript that aims to bring static types to modern JavaScript. The TypeScript compiler reads in TypeScript code, which has things like type declarations and type annotations, and emits clean readable JavaScript with those constructs transformed and removed. That code runs in any ECMAScript runtime like your favorite browsers and Node.js. At its core, this experience means analyzing your code to catch things like bugs and typos before your users run into them; but it brings more than that. Thanks to all that information and analysis TypeScript can provide a better authoring experience, providing code completion and navigation features like Find all References, Go to Definition, and Rename in your favorite editor.

To get started with the language itself, check out typescriptlang.org to learn more. And if you want to try TypeScript 3.0 out now, you can get it through NuGet or via npm by running

npm install -g typescript

You can also get editor support for

  • Visual Studio 2017 (for version 15.2 or later).
  • Visual Studio 2015 (which requires update 3).
  • For Visual Studio Code by installing the Insiders release until the full release provides it.
  • Sublime Text 3 via PackageControl.

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

The 3.0 Journey

When we released TypeScript 2.0, we took a brief look back at how each release leading up to TypeScript 2.0 brought the language to where it is today. Between TypeScript 1.0 and up until 2.0, the language added union types, type guards, modern ECMAScript support, type aliases, JSX support, literal types, and polymorphic this types. If we include TypeScript 2.0 with its introduction of non-nullable types, control flow analysis, tagged union support, this-types, and a simplified model around .d.ts file acquisition, that era truly defined the fundamentals of using TypeScript.

So what have we done since? What, apart from new ECMAScript features like the long-await-ed async/await, generators, and object rest/spread brought us to TypeScript 3.0?

TypeScript 2.1 was a foundational release that introduced a static model for metaprogramming in JavaScript. The key query (keyof), indexed access (T[K]), and mapped object ({ [K in keyof T]: T[K] }) types have been instrumental in better modeling libraries like React, Ember, Lodash, and more.

TypeScript 2.2 and 2.3 brought support for mixin patterns, the non-primitive object type, and generic defaults, used by a number of projects like Angular Material and Polymer. TypeScript 2.3 also shipped a feature for fine-grained control of this types that allowed TypeScript to work well with libraries like Vue, and added the checkJs flag to enable type-checking on JavaScript files.

TypeScript 2.4 and 2.6 tightened up the story for strict checking on Function types, addressing some of the longest-standing feedback about our type system through --strictFunctionTypes which enforced contravariance on parameters. 2.7 continued the trend of strictness with --strictPropertyInitialization checks in classes.

TypeScript 2.8 introduced conditional types, a powerful tool for statically expressing decisions based on types, and 2.9 generalized keyof and provided easier imports for types.

Which brings us to TypeScript 3.0! Despite the new big number, 3.0 has few breaking changes (meaning it should be very easy to upgrade) and introduces a new flexible and scalable way to structure your projects, powerful new support for operating on parameter lists, new types to enforce explicit checks, better JSX support, an overall better error UX, and much more!

What's New?

  • Project references
    • --build mode
    • Controlling output structure
    • Further work
  • Extracting and spreading parameter lists with tuples
  • Richer tuple types
  • The unknown type
  • Improved errors and UX
    • Related error spans
    • Improved messages and elaboration
  • Support for defaultProps in JSX
  • /// directives
  • Editor Productivity
    • Named import refactorings
    • Closing JSX tag completions and outlining spans
    • Quick fixes for unreachable code and unused labels
  • Breaking changes
    • unknown is a reserved type name
    • API breaking changes

Project references

It's fairly common to have several different build steps for a library or application. Maybe your codebase has a src and a test directory. Maybe you have your front-end code in a folder called client with your Node.js back-end code in a folder called server, and each imports code from a shared folder. And maybe you use what's called a "monorepo" and have many many projects which depend on each other in non-trivial ways.

One of the biggest features that we've worked on for TypeScript 3.0 is called "project references", and it aims to make working with these scenarios easier.

Project references allow TypeScript projects to depend on other TypeScript projects - specifically, allowing tsconfig.json files to reference other tsconfig.json files. Specifying these dependencies makes it easier to split your code into smaller projects, since it gives TypeScript (and tools around it) a way to understand build ordering and output structure. That means things like faster builds that work incrementally, and support for transparently navigating, editing, and refactoring across projects. Since 3.0 lays the foundation and exposes the APIs, any build tool should be able to provide this.

What's it look like?

As a quick example, here's what a tsconfig.json with project references looks like:

// ./src/bar/tsconfig.json
{
    "compilerOptions": {
        // Needed for project references.
        "composite": true,
        "declaration": true,

        // Other options...
        "outDir": "../../lib/bar",
        "strict": true, "module": "esnext", "moduleResolution": "node",
    },
    "references": [
        { "path": "../foo" }
    ]
}

There are two new fields to notice here: composite and references.

references simply specifies other tsconfig.json files (or folders immediately containing them). Each reference is currently just an object with a path field, and lets TypeScript know that building the current project requires building that referenced project first.

Perhaps equally important is the composite field. The composite field ensures certain options are enabled so that this project can be referenced and built incrementally for any project that depends on it. Being able to intelligently and incrementally rebuild is important, since build speed is one of the reasons you might break up a project in the first place. For example, if project front-end depends on shared, and shared depends on core, our APIs around project references can be used to detect a change in core, but to only rebuild shared if the types (i.e. the .d.ts files) produced by core have changed. That means a change to core doesn't completely force us to rebuild the world. For that reason, setting composite forces the declaration flag to be set as well.

--build mode

TypeScript 3.0 will provide a set of APIs for project references so that other tools can provide this fast incremental behavior. As an example, gulp-typescript already leverages it! So project references should be able to integrate with your choice of build orchestrators in the future.

However, for many simple apps and libraries, it's nice not to need external tools. That's why tsc now ships with a new --build flag.

tsc --build (or its nickname, tsc -b) takes a set of projects and builds them and their dependencies. When using this new build mode, the --build flag has to be set first, and can be paired with certain other flags:

  • --verbose: displays every step of what a build requires
  • --dry: performs a build without emitting files (this is useful with --verbose)
  • --clean: attempts to remove output files given the inputs
  • --force: forces a full non-incremental rebuild for a project

Controlling output structure

One subtle but incredibly useful benefit of project references is logically being able to map your input source to its outputs.

If you've ever tried to share TypeScript code between the client and server of your application, you might have run into problems controlling the output structure.

For example, if client/index.ts and server/index.ts both reference shared/index.ts for the following projects:

src
├── client
│   ├── index.ts
│   └── tsconfig.json
├── server
│   ├── index.ts
│   └── tsconfig.json
└── shared
    └── index.ts

...then trying to build client and server, we'll end up with...

lib
├── client
│   ├── client
│   │   └── index.js
│   └── shared
│       └── index.js
└── server
    ├── server
    │   └── index.js
    └── shared
        └── index.js

rather than

lib
├── client
│   └── index.js
├── shared
│   └── index.js
└── server
    └── index.js

Notice that we ended up with a copy of shared in both client and server. We unnecessarily spent time building shared twice and introduced an undesirable level of nesting in lib/client/client and lib/server/server.

The problem is that TypeScript greedily looks for .ts files and tries to include them in a given compilation. Ideally, TypeScript would understand that these files don't need to be built in the same compilation, and would instead jump to the .d.ts files for type information.

Creating a tsconfig.json for shared and using project references does exactly that. It signals to TypeScript that

  1. shared should be built independently, and that
  2. when importing from ../shared, we should look for the .d.ts files in its output directory.

This avoids triggering a double-build, and also avoids accidentally absorbing all the contents of shared.

Further work

To get a deeper understanding of project references and how you can use them, read up more our issue tracker. In the near future, we'll have documentation on project references and build mode.

We're committed to ensuring that other tool authors can support project references, and will continue to improve the editing experience around project references. Our intent is for project references to feel as seamless as authoring code with a single tsconfig.json. If you do end up using project references, we'd appreciate any and all feedback to do just that.

Extracting and spreading parameter lists with tuples

We often take it for granted, but JavaScript lets us think about parameter lists as first-class values - either by using arguments or rest-parameters (e.g. ...rest).

function call(fn, ...args) {
    return fn(...args);
}

Notice here that call works on functions of any parameter length. Unlike other languages, JavaScript doesn't force us to define a call0, call1, call2, etc. as follows:

function call0(fn) {
    return fn();
}

function call1(fn, param1) {
    return fn(param1);
}

function call2(fn, param1, param2) {
    return fn(param1, param2);
}

function call3(fn, param1, param2, param3) {
    return fn(param1, param2, param3);
}

Unfortunately, for a while there wasn't a great well-typed way to express this statically in TypeScript without declaring a finite number of overloads:

// TODO (billg): 5 overloads should *probably* be enough for anybody?
function callT1, T2, T3, T4, R>(fn: (param1: T1, param2: T2, param3: T3, param4: T4) => R, param1: T1, param2: T2, param3: T3, param4: T4): R
function callT1, T2, T3, R>(fn: (param1: T1, param2: T2, param3: T3) => R, param1: T1, param2: T2, param3: T3): R
function callT1, T2, R>(fn: (param1: T1, param2: T2) => R, param1: T1, param2: T2): R
function callT1, R>(fn: (param1: T1) => R, param1: T1): R;
function callR>(fn: () => R, param1: T1): R;
function call(fn: (...args: any[]) => any, ...args: any[]) {
    return fn(...args);
}

Oof! Another case of death by a thousand overloads! Or at least, as many overloads as our users asked us for.

TypeScript 3.0 allows us to better model scenarios like these by now allowing rest parameters to be generic, and inferring those generics as tuple types! Instead of declaring each of these overloads, we can say that the ...args rest parameter from fn must be a type parameter that extends an array, and then we can re-use that for the ...args that call passes:

function callTS extends any[], R>(fn: (...args: TS) => R, ...args: TS): R {
    return fn(...args);
}

When we call the call function, TypeScript will try to extract the parameter list from whatever we pass to fn, and turn that into a tuple:

function foo(x: number, y: string): string {
    return (x + y).toLowerCase();
}

// The `TS` type parameter is inferred as `[number, string]`
call(foo, 100, "hello");

When TypeScript infers TS as [number, string] and we end up re-using TS on the rest parameter of call, the instantiation looks like the following

function call(fn: (...args: [number, string]) => string, ...args: [number, string]): string

And with TypeScript 3.0, using a tuple in a rest parameter gets flattened into the rest of the parameter list! The above boils down to simple parameters with no tuples:

function call(fn: (arg1: number, arg2: string) => string, arg1: number, arg2: string): string

So in addition to catching type errors when we pass in the wrong arguments:

function callTS extends any[], R>(fn: (...args: TS) => R, ...args: TS): R {
    return fn(...args);
}

call((x: number, y: string) => y, "hello", "world");
//                                ~~~~~~~
// Error! `string` isn't assignable to `number`!

and inference from other arguments:

call((x, y) => { /* .... */ }, "hello", 100);
//    ^  ^
// `x` and `y` have their types inferred as `string` and `number` respectively.

we can also observe the tuple types that these functions infer from the outside:

function tupleTS extends any[]>(...xs: TS): TS {
    return xs;
}

let x = tuple(1, 2, "hello"); // has type `[number, number, string]

There is a subtler point to note though. In order to make all of this work, we needed to expand what tuples could do...

Richer tuple types

To make tuples model parameter lists (as we just discussed), we had to rethink tuple types a bit. Before TypeScript 3.0, the best that tuples could model was the order and count of a set of parameters.

However, parameter lists aren't just ordered lists of types. For example, parameters at the end can be optional:

// Both `y` and `z` are optional here.
function foo(x: boolean, y = 100, z?: string) {
    // ...
}

foo(true);
foo(true, undefined, "hello");
foo(true, 200);

The last parameter can be a rest parameter.

// `rest` accepts any number of strings - even none!
function foo(...rest: string[]) {
    // ...
}

foo();
foo("hello");
foo("hello", "world");

And finally, there is one mildly interesting property about parameter lists which is that they can be empty:

// Accepts no parameters.
function foo() {
    // ...
}

foo();

So to make it possible for tuples to correspond to parameter lists, we needed to model each of these scenarios.

First, tuples now allow trailing optional elements:

/**
 * 2D, or potentially 3D, coordinate.
 */
type Coordinate = [number, number, number?];

The Coordinate type creates a tuple with an optional property named 2 - the element at index 2 might not be defined! Interestingly, since tuples use numeric literal types for their length properties, Coordinate's length property has the type 2 | 3.

Second, tuples now allow rest elements at the end.

type OneNumberAndSomeStrings = [number, ...string[]];

Rest elements introduce some interesting open-ended behavior to tuples. The above OneNumberAndSomeStrings type requires its first property to be a number, and permits 0 or more strings. Indexing with an arbitrary number will return a string | number since the index won't be known. Likewise, since the tuple length won't be known, the length property is just number.

Of note, when no other elements are present, a rest element in a tuple is identical to itself:

type Foo = [...number[]]; // Equivalent to `number[]`.

Finally, tuples can now be empty! While it's not that useful outside of parameter lists, the empty tuple type can be referenced as []:

type EmptyTuple = [];

As you might expect, the empty tuple has a length of 0 and indexing with a number returns the never type.

Improved errors and UX

Over time we've heard more and more demand from our community regarding better error messages. While we're by no means done, we heard you in TypeScript 3.0 and have invested a bit here.

Part of the goal of providing a good error message is also guiding a user towards a way to fix the error, or providing a way to intuit why the error message was given in the first place. Much of the time, there can be a lot of information or multiple reasons an error message might surface. Of those reasons, we might find they come from different parts of the code.

Related error spans are a new way to surface that information to users. In TypeScript 3.0, error messages can provide messages on other locations so that users can reason about cause-and-effect of an error.

In some sense, related error messages can give a user not just an explanation, but also breadcrumbs to see where things went wrong.

These spans will also appear in the terminal when running tsc with --pretty mode enabled, though our team is still iterating on the UI and would appreciate feedback!

Improved messages and elaboration

Around TypeScript 2.9, we started investing more in our error messages, and with 3.0 we really tried to tackle a core set of cases that could give a smarter, cleaner, and more accurate error experience. This includes things like picking better types with mismatches in union types, and cutting right to the chase for certain error messages.

We believe this effort had paid off and will provide significantly shorter and cleaner error messages.

The unknown type

The any type is the most-capable type in TypeScript - while it encompasses the type of every possible value, it doesn't force us to do any checking before we try to call, construct, or access properties on these values. It also lets us assign values of type any to values that expect any other type.

This is mostly useful, but it can be a bit lax.

let foo: any = 10;

// All of these will throw errors, but TypeScript
// won't complain since `foo` has the type `any`.
foo.x.prop;
foo.y.prop;
foo.z.prop;
foo();
new foo();
upperCase(foo);
foo `hello world!`;

function upperCase(x: string) {
    return x.toUpperCase();
}

There are often times where we want to describe the least-capable type in TypeScript. This is useful for APIs that want to signal "this can be any value, so you must perform some type of checking before you use it". This forces users to safely introspect returned values.

TypeScript 3.0 introduces a new type called unknown that does exactly that. Much like any, any value is assignable to unknown; however, unlike any, unknown is assignable to almost nothing else without a type assertion. You also can't access any properties off of an unknown, nor can you call/construct them.

As an example, swapping the above example to use unknown instead of any forces turns all usages of foo into an error:

let foo: unknown = 10;

// Since `foo` has type `unknown`, TypeScript
// errors on each of these locations.
foo.x.prop;
foo.y.prop;
foo.z.prop;
foo();
new foo();
upperCase(foo);
foo `hello world!`;

function upperCase(x: string) {
    return x.toUpperCase();
}

Instead, we're now forced to either perform checking, or use a type assertion to convince the type-system that we know better.

let foo: unknown = 10;

function hasXYZ(obj: any): obj is { x: any, y: any, z: any } {
    return !!obj &&
        typeof obj === "object" &&
        "x" in obj && "y" in obj && "z" in obj
}

// Using a user-defined type guard...
if (hasXYZ(foo)) {
    // ...we're allowed to access certain properties again.
    foo.x.prop;
    foo.y.prop;
    foo.z.prop;
}

// We can also just convince TypeScript we know what we're doing
// by using a type assertion.
upperCase(foo as string);

function upperCase(x: string) {
    return x.toUpperCase();
}

Note that if you've been using a type like {} | null | undefined to achieve similar behavior, unknown usually has more desirable behavior in constructs like conditional types, since conditional types distribute across unions:

type ArrayifyT> = T extends any ? ArrayT> : never;

type A = Arrayify| null | undefined>; // null[] | undefined[] | {}[]
type B = Arrayifyunknown>;               // unknown[]

Support for defaultProps in JSX

Note: at the time of writing, React's .d.ts files may not yet support this functionality.

If you've ever used default initializers in modern TypeScript/JavaScript, you might know how handy they can be for function callers. They give us a useful syntax to let callers use functions more easily by not requiring certain arguments, while letting function authors ensure that their values are always defined in a clean way.

function loudlyGreet(name = "world") {
    // Thanks to the default initializer, `name` will always have type `string` internally.
    // We don't have to check for `undefined` here.
    console.log("HELLO", name.toUpperCase());
}

// Externally, `name` is optional, and we can potentially pass `undefined` or omit it entirely.
loudlyGreet();
loudlyGreet(undefined);

In React, a similar concept exists for components and their props. When creating a new element using a component, React looks up a property called defaultProps, to fill in values for props that are omitted.

// Some non-TypeScript JSX file

import * as React from "react";
import * as ReactDOM from "react-dom";

export class Greet extends React.Component {
    render() {
        const { name } = this.props;
        return div>Hello ${name.toUpperCase()}!div>;
    }

    static defaultProps = {
        name: "world",
    };
}

//      Notice no `name` attribute was specified!
//                                     vvvvvvvvv
const result = ReactDOM.renderToString(Greet />);
console.log(result);

Notice that in , name didn't have to be specified. When a Greet element is created, name will be initialized with "world" and this code will print

Hello world!
.

Unfortunately, TypeScript didn't understand that defaultProps had any bearing on JSX invocations. Instead, users would often have to declare properties optional and use non-null assertions inside of render:

export interface Props { name?: string }
export class Greet extends React.$1ComponentProps> {
    render() {
        const { name } = this.props;

        // Notice the `!` ------v
        return div>Hello ${name!.toUpperCase()}!/div>;
    }
    static defaultProps = { name: "world"}
}

Or they'd use some hacky type-assertions to fix up the type of the component before exporting it.

That's why TypeScript 3.0, the language supports a new type alias in the JSX namespace called LibraryManagedAttributes. Despite the long name, this is just a helper type that tells TypeScript what attributes a JSX tag accepts. The short story is that using this general type, we can model React's specific behavior for things like defaultProps and, to some extent, propTypes.

export interface Props {
    name: string
}

export class Greet extends React.$1ComponentProps> {
    render() {
        const { name } = this.props;
        return div>Hello ${name.toUpperCase()}!/div>;
    }
    static defaultProps = { name: "world"}
}

// Type-checks! No type assertions needed!
let el = Greet />

Keep in mind that there are some limitations. For defaultProps that explicitly specify their type as something like Partial, or stateless function components (SFCs) whose defaultProps are declared with Partial, will make all props optional. As a workaround, you can omit the type annotation entirely for defaultProps on a class component (like we did above), or use ES2015 default initializers for SFCs:

function Greet({ name = "world" }: Props) {
    return div>Hello ${name.to

Share the post

Announcing TypeScript 3.0

×

Subscribe to Msdn Blogs | Get The Latest Information, Insights, Announcements, And News From Microsoft Experts And Developers In The Msdn Blogs.

Get updates delivered right to your inbox!

Thank you for your subscription

×