Announcing TypeScript 3.4 RC

Avatar

Daniel

Today we’re happy to announce the availability of our release candidate (RC) of TypeScript 3.4. Our hope is to collect feedback and early issues to ensure our final release is simple to pick up and use right away.

To get started using the RC, you can get it through NuGet, or use npm with the following command:

npm install -g typescript@rc

You can also get editor support by

Let’s explore what’s new in 3.4!

Faster subsequent builds with the --incremental flag

Because TypeScript files are compiled, it introduces an intermediate step between writing and running your code. One of our goals is to minimize built time given any change to your program. One way to do that is by running TypeScript in --watch mode. When a file changes under --watch mode, TypeScript is able to use your project’s previously-constructed dependency graph to determine which files could potentially have been affected and need to be re-checked and potentially re-emitted. This can avoid a full type-check and re-emit which can be costly.

But it’s unrealistic to expect all users to keep a tsc --watch process running overnight just to have faster builds tomorrow morning. What about cold builds? Over the past few months, we’ve been working to see if there’s a way to save the appropriate information from --watch mode to a file and use it from build to build.

TypeScript 3.4 introduces a new flag called --incremental which tells TypeScript to save information about the project graph from the last compilation. The next time TypeScript is invoked with --incremental, it will use that information to detect the least costly way to type-check and emit changes to your project.

// tsconfig.json
{
    "compilerOptions": {
        "incremental": true,
        "outDir": "./lib"
    },
    "include": ["./src"]
}

By default with these settings, when we run tsc, TypeScript will look for a file called .tsbuildinfo in our output directory (./lib). If ./lib/.tsbuildinfo doesn’t exist, it’ll be generated. But if it does, tsc will try to use that file to incrementally type-check and update our output files.

These .tsbuildinfo files can be safely deleted and don’t have any impact on our code at runtime – they’re purely used to make compilations faster. We can also name them anything that we want, and place them anywhere we want using the --tsBuildInfoFile flag.

// front-end.tsconfig.json
{
    "compilerOptions": {
        "incremental": true,
        "tsBuildInfoFile": "./buildcache/front-end",
        "outDir": "./lib"
    },
    "include": ["./src"]
}

As long as nobody else tries writing to the same cache file, we should be able to enjoy faster incremental cold builds.

Composite projects

Part of the intent with composite projects (tsconfig.jsons with composite set to true) is that references between different projects can be built incrementally. As such, composite projects will always produce .tsbuildinfo files.

outFile

When outFile is used, the build information file’s name will be based on the output file’s name. As an example, if our output JavaScript file is ./output/foo.js, then under the --incremental flag, TypeScript will generate the file ./output/foo.tsbuildinfo. As above, this can be controlled with the --tsBuildInfoFile flag.

The --incremental file format and versioning

While the file generated by --incremental is JSON, the file isn’t mean to be consumed by any other tool. We can’t provide any guarantees of stability for its contents, and in fact, our current policy is that any one version of TypeScript will not understand .tsbuildinfo files generated from another version.

Improvements for ReadonlyArray and readonly tuples

TypeScript 3.4 makes it a little bit easier to use read-only array-like types.

A new syntax for ReadonlyArray

The ReadonlyArray type describes Arrays that can only be read from. Any variable with a handle to a ReadonlyArray can’t add, remove, or replace any elements of the array.

function foo(arr: ReadonlyArray<string>) {
    arr.slice();        // okay
    arr.push("hello!"); // error!
}

While it’s often good practice to use ReadonlyArray over Array for the purpose of intent, it’s often been a pain given that arrays have a nicer syntax. Specifically, number[] is a shorthand version of Array<number>, just as Date[] is a shorthand for Array<Date>.

TypeScript 3.4 introduces a new syntax for ReadonlyArray using a new readonly modifier for array types.

function foo(arr: readonly string[]) {
    arr.slice();        // okay
    arr.push("hello!"); // error!
}

readonly tuples

TypeScript 3.4 also introduces new support for readonly tuples. We can prefix any tuple type with the readonly keyword to make it a readonly tuple, much like we now can with array shorthand syntax. As you might expect, unlike ordinary tuples whose slots could be written to, readonly tuples only permit reading from those positions.

function foo(pair: readonly [string, string]) {
    console.log(pair[0]);   // okay
    pair[1] = "hello!";     // error
}

The same way that ordinary tuples are types that extend from Array – a tuple with elements of type T1, T2, … Tn extends from Array< T1 | T2 | … Tn >readonly tuples are types that extend from ReadonlyArray. So a readonly tuple with elements T1, T2, … Tn extends from ReadonlyArray< T1 | T2 | … Tn >.

readonly mapped type modifiers and readonly arrays

In earlier versions of TypeScript, we generalized mapped types to operate differently on array-like types. This meant that a mapped type like Boxify could work on arrays and tuples alike.

interface Box<T> { value: T }

type Boxify<T> = {
    [K in keyof T]: Box<T[K]>
}

// { a: Box<string>, b: Box<number> }
type A = Boxify<{ a: string, b: number }>;

// Array<Box<number>>
type B = Boxify<number[]>;

// [Box<string>, Box<number>]
type C = Boxify<[string, boolean]>;

Unfortunately, mapped types like the Readonly utility type were effectively no-ops on array and tuple types.

// lib.d.ts
type Readonly<T> = {
    readonly [K in keyof T]: T[K]
}

// How code acted *before* TypeScript 3.4

// { readonly a: string, readonly b: number }
type A = Readonly<{ a: string, b: number }>;

// number[]
type B = Readonly<number[]>;

// [string, boolean]
type C = Readonly<[string, boolean]>;

In TypeScript 3.4, the readonly modifier in a mapped type will automatically convert array-like types to their corresponding readonly counterparts.

// How code acts now *with* TypeScript 3.4

// { readonly a: string, readonly b: number }
type A = Readonly<{ a: string, b: number }>;

// readonly number[]
type B = Readonly<number[]>;

// readonly [string, boolean]
type C = Readonly<[string, boolean]>;

Similarly, you could write a utility type like Writable mapped type that strips away readonly-ness, and that would convert readonly array containers back to their mutable equivalents.

type Writable<T> = {
    -readonly [K in keyof T]: T[K]
}

// { a: string, b: number }
type A = Writable<{
    readonly a: string;
    readonly b: number
}>;

// number[]
type B = Writable<readonly number[]>;

// [string, boolean]
type C = Writable<readonly [string, boolean]>;

Caveats

Despite its appearance, the readonly type modifier can only be used for syntax on array types and tuple types. It is not a general-purpose type operator.

let err1: readonly Set<number>; // error!
let err2: readonly Array<boolean>; // error!

let okay: readonly boolean[]; // works fine

const assertions

When declaring a mutable variable or property, TypeScript often widens values to make sure that we can assign things later on without writing an explicit type.

let x = "hello";

// hurray! we can assign to 'x' later on!
x = "world";

Technically, every literal value has a literal type. Above, the type "hello" got widened to the type string before inferring a type for x.

One alternative view might be to say that x has the original literal type "hello" and that we can’t assign "world" later on like so:

let x: "hello" = "hello";

// error!
x = "world";

In this case, that seems extreme, but it can be useful in other situations. For example, TypeScripters often create objects that are meant to be used in discriminated unions.

type Shape =
    | { kind: "circle", radius: number }
    | { kind: "square", sideLength: number }

function getShapes(): readonly Shape[] {
    let result = [
        { kind: "circle", radius: 100, },
        { kind: "square", sideLength: 50, },
    ];
    
    // Some terrible error message because TypeScript inferred
    // 'kind' to have the type 'string' instead of
    // either '"circle"' or '"square"'.
    return result;
}

Mutability is one of the best heuristics of intent which TypeScript can use to determine when to widen (rather than analyzing our entire program).

Unfortunately, as we saw in the last example, in JavaScript properties are mutable by default. This means that the language will often widen types undesirably, requiring explicit types in certain places.

function getShapes(): readonly Shape[] {
    // This explicit annotation gives a hint
    // to avoid widening in the first place.
    let result: readonly Shape[] = [
        { kind: "circle", radius: 100, },
        { kind: "square", sideLength: 50, },
    ];
    
    return result;
}

Up to a certain point this is okay, but as our data structures get more and more complex, this becomes cumbersome.

To solve this, TypeScript 3.4 introduces a new construct for literal values called const assertions. Its syntax is a type assertion with const in place of the type name (e.g. 123 as const). When we construct new literal expressions with const assertions, we can signal to the language that

  • no literal types in that expression should be widened (e.g. no going from "hello" to string)
  • object literals get readonly properties
  • array literals become readonly tuples
// Type '10'
let x = 10 as const;

// Type 'readonly [10, 20]'
let y = [10, 20] as const;

// Type '{ readonly text: "hello" }'
let z = { text: "hello" } as const;

Outside of .tsx files, the angle bracket assertion syntax can also be used.

// Type '10'
let x = <const>10;

// Type 'readonly [10, 20]'
let y = <const>[10, 20];

// Type '{ readonly text: "hello" }'
let z = <const>{ text: "hello" };

This feature often means that types that would otherwise be used just to hint immutability to the compiler can often be omitted.

// Works with no types referenced or declared.
// We only needed a single const assertion.
function getShapes() {
    let result = [
        { kind: "circle", radius: 100, },
        { kind: "square", sideLength: 50, },
    ] as const;
    
    return result;
}

for (const shape of getShapes()) {
    // Narrows perfectly!
    if (shape.kind === "circle") {
        console.log("Circle radius", shape.radius);
    }
    else {
        console.log("Square side length", shape.sideLength);
    }
}

Notice the above needed no type annotations. The const assertion allowed TypeScript to take the most specific type of the expression.

Caveats

One thing to note is that const assertions can only be applied immediately on simple literal expressions.

// Error!
//   A 'const' assertion can only be applied to a string, number, boolean, array, or object literal.
let a = (Math.random() < 0.5 ? 0 : 1) as const;

// Works!
let b = Math.random() < 0.5 ?
    0 as const :
    1 as const;

Another thing to keep in mind is that const contexts don’t immediately convert an expression to be fully immutable.

let arr = [1, 2, 3, 4];

let foo = {
    name: "foo",
    contents: arr,
};

foo.name = "bar";   // error!
foo.contents = [];  // error!

foo.contents.push(5); // ...works!

Type-checking for globalThis

It can be surprisingly difficult to access or declare values in the global scope, perhaps because we’re writing our code in modules (whose local declarations don’t leak by default), or because we might have a local variable that shadows the name of a global value. In different environments, there are different ways to access what’s effectively the global scope – global in Node, window, self, or frames in the browser, or this in certain locations outside of strict mode. None of this is obvious, and often leaves users feeling unsure of whether they’re writing correct code.

TypeScript 3.4 introduces support for type-checking ECMAScript’s new globalThis – a global variable that, well, refers to the global scope. Unlike the above solutions, globalThis provides a standard way for accessing the global scope which can be used across different environments.

// in a global file:

let abc = 100;

// Refers to 'abc' from above.
globalThis.abc = 200;

globalThis is also able to reflect whether or not a global variable was declared as a const by treating it as a readonly property when accessed.

const answer = 42;

globalThis.answer = 333333; // error!

It’s important to note that TypeScript doesn’t transform references to globalThis when compiling to older versions of ECMAScript. As such, unless you’re targeting evergreen browsers (which already support globalThis), you may want to use an appropriate polyfill instead.

Convert to named parameters

Sometimes, parameter lists start getting unwieldy.

function updateOptions(
    hue?: number,
    saturation?: number,
    brightness?: number,
    positionX?: number,
    positionY?: number
    positionZ?: number) {
    
    // ....
}

In the above example, it’s way too easy for a caller to mix up the order of arguments given. A common JavaScript pattern is to instead use an “options object”, so that each option is explicitly named and order doesn’t ever matter. This emulates a feature that other languages have called “named parameters”.

interface Options {
    hue?: number,
    saturation?: number,
    brightness?: number,
    positionX?: number,
    positionY?: number
    positionZ?: number
}

function updateOptions(options: Options = {}) {
    
    // ....
}

The TypeScript team doesn’t just work on a compiler – we also provide the functionality that editors use for rich features like completions, go to definition, and refactorings. In TypeScript 3.4, our intern Gabriela Britto has implemented a new refactoring to convert existing functions to use this “named parameters” pattern.

A refactoring being applied to a function to make it take named parameters.

While we may change the name of the feature by our final 3.4 release and we believe there may be room for some of the ergonomics, we would love for you to try the feature out and give us your feedback.

Breaking changes

Top-level this is now typed

The type of top-level this is now typed as typeof globalThis instead of any. As a consequence, you may receive errors for accessing unknown values on this under noImplicitAny.

// previously okay in noImplicitAny, now an error
this.whargarbl = 10;

Note that code compiled under noImplicitThis will not experience any changes here.

Propagated generic type arguments

In certain cases, TypeScript 3.4’s improved inference might produce functions that are generic, rather than ones that take and return their constraints (usually {}).

declare function compose<T, U, V>(f: (arg: T) => U, g: (arg: U) => V): (arg: T) => V;

function list<T>(x: T) { return [x]; }
function box<T>(value: T) { return { value }; }

let f = compose(list, box);
let x = f(100)

// In TypeScript 3.4, 'x.value' has the type
//
//   number[]
//
// but it previously had the type
//
//   {}[]
//
// So it's now an error to push in a string.
x.value.push("hello");

An explicit type annotation on x can get rid of the error.

What’s next?

TypeScript 3.4 is our first release that has had an iteration plan outlining our plans for this release, which is meant to align with our 6-month roadmap. You can keep an eye on both of those, and on our rolling feature roadmap page for any upcoming work.

Right now we’re looking forward to hearing about your experience with the RC, so give it a shot now and let us know your thoughts!

– Daniel Rosenwasser and the TypeScript team

Avatar
Daniel Rosenwasser

Program Manager, TypeScript

Follow Daniel   

Avatar
Andrew Bradley 2019-03-15 18:28:14
I think the final const assertion example is missing the `as const`.
Ingo Bürk
Ingo Bürk 2019-03-15 21:35:02
Isn't the introduction of readonly for tuples and arrays also a breaking change? The article itself explained the difference of what Readonly<T> means for these before and after the change  
Mathias Kahl
Mathias Kahl 2019-03-16 03:05:35
The `const` assertions and the new `readonly` keyword are super convenient! Also, if propagation of generic type arguments actually works that would be great! I continue to be impressed by TypeScript's awesome type inference, good work!
Avatar
Tim Mackey 2019-03-18 14:13:16
This RC version is failing with "ERROR in The Angular Compiler requires TypeScript >=3.1.1 and <3.3.0 but 3.4.0-rc was found instead." Any suggestions to resolve?  What versions of node, etc. might this RC be dependent on? My node version is 10.15.3 (latest), Angular CLI: 7.3.6, Angular: 7.2.9, OS: win32 x64.