Announcing TypeScript 3.2

Daniel Rosenwasser

TypeScript 3.2 is here today!If you’re unfamiliar with TypeScript, it’s a language that brings static type-checking to JavaScript so that you can catch issues before you even run your code – or before you even save your file. It also includes the latest JavaScript features from the ECMAScript standard on older browsers and runtimes by compiling those features into a form that they understand. But beyond type-checking and compiling your code, TypeScript also provides tooling in your favorite editor so that you can jump to the definition of any variable, find who’s using a given function, and automate refactorings and fixes to common problems. TypeScript even provides this for JavaScript users (and can also type-check JavaScript code typed with JSDoc), so if you’ve used editors like Visual Studio or Visual Studio Code on a .js file, TypeScript is powering that experience.

To get started with the language itself, check out typescriptlang.org to learn more.

But if you want to try TypeScript 3.2 out now, you can get it through NuGet or via npm by running

npm install -g typescript

You can also get editor support for

Other editors may have different update schedules, but should all have TypeScript available soon.

We have some important information below for NuGet users and Visual Studio 2015 users, so please continue reading if you use either product.

Below we have a bit about what’s new in 3.2.

strictBindCallApply

As you might’ve guessed from the title of this section, TypeScript 3.2 introduces stricter checking for bind, call, and apply. But what does that mean?

Well, in JavaScript, bind, call, and apply are methods on functions that allow us to do things like bind this and partially apply arguments, call functions with a different value for this, and call functions with an array for their arguments.

Unfortunately, in its earlier days, TypeScript lacked the power to model these functions, and bind, call, and apply were all typed to take any number of arguments and returned any. Additionally, ES2015’s arrow functions and rest/spread arguments gave us a new syntax that made it easier to express what some of these methods do – and in a more efficient way as well.

Still, demand to model these patterns in a type-safe way led us to revisit this problem recently. We realized that two features opened up the right abstractions to accurately type bind, call, and apply without any hard-coding:

  1. this parameter types from TypeScript 2.0
  2. Modeling parameter lists with tuple types from TypeScript 3.0

Combined, the two of of them can ensure our uses of bind, call, and apply are more strictly checked when we use a new flag called strictBindCallApply. When using this new flag, the methods on callable objects are described by a new global type called CallableFunction which declares stricter versions of the signatures for bind, call, and apply. Similarly, any methods on constructable (but not callable) objects are described by a new global type called NewableFunction.

As an example, we can look at how Function.prototype.apply acts under this behavior:

function foo(a: number, b: string): string {
    return a + b;
}

let a = foo.apply(undefined, [10]);              // error: too few argumnts
let b = foo.apply(undefined, [10, 20]);          // error: 2nd argument is a number
let c = foo.apply(undefined, [10, "hello", 30]); // error: too many arguments
let d = foo.apply(undefined, [10, "hello"]);     // okay! returns a string

 

Needless to say, whether you do any sophisticated metaprogramming, or you use simple patterns like binding methods in your class instances (this.foo = this.foo.bind(this)), this feature can help catch a lot of bugs. For more details, you can check out the original pull request here.

Caveats

One caveat of this new functionality is that due to certain limitations, bind, call, and apply can’t yet fully model generic functions or functions that have overloads. When using these methods on a generic function, type parameters will be substituted with the empty object type ({}), and when used on a function with overloads, only the last overload will ever be modeled.

Object spread on generic types

JavaScript supports a handy way of copying existing properties from an existing object into a new one called “spreads”. To spread an existing object into a new object, you define an element with three consecutive periods (...) like so:

let person = { name: "Daniel", location: "New York City" };

// My secret revealed, I have two clones!
let shallowCopyOfPerson = { ...person };
let shallowCopyOfPersonWithDifferentLocation = { ...person, location: "Seattle" };
TypeScript does a pretty good job here when it has enough information about the type. The type system closely tries to model the behavior of spreads and overwrites new properties, tries to ignore methods, etc. But unfortunately up until now it wouldn't work with generics at all.

function merge<T, U>(x: T, y: U) {
    // Previously an error!
    return { ...x, ...y };
}

 

This was an error because we had no way to express the return type of merge. There was no syntax (nor semantics) that could express two unknown types being spread into a new one.

We could have come up with a new concept in the type system called an “object spread type”, and in fact we had a proposal for exactly that. Essentially this would be a new type operator that looks like { ...T, ...U } to reflect the syntax of an object spread. When both T and U are known, that type would flatten down to some new object type.

However, this is pretty complex and requires adding new rules to type relationships and inference. While we explored several different avenues, we recently arrived at two conclusions:

  1. For most uses of spreads in JavaScript, users were fine modeling the behavior with intersection types (i.e. Foo & Bar).
  2. Object.assign – a function that exhibits most of the behavior of spreading objects – is already modeled using intersection types, and we’ve seen very little negative feedback around that.

Given that intersections model the common cases, and that they’re relatively easy to reason about for both users and the type system, TypeScript 3.2 now permits object spreads on generics and models them using intersections:

// Returns 'T & U'
function merge<T, U>(x: T, y: U) {
    return { ...x, ...y };
}

// Returns '{ name: string, age: number, greeting: string } & T'
function foo<T>(obj: T) {
    let person = {
        name: "Daniel",
        age: 26
    };

    return { ...person, greeting: "hello", ...obj };
}

 

Object rest on generic types

Object rest patterns are sort of the dual of object spreads. Instead of creating a new object with some extra/overridden properties, it creates a new object that lacks some specified properties.

let { x, y, z, ...rest } = obj;

 

In the above, the most intuitive way to look at this code is that rest copies over all the properties from obj apart from x, y, and z. For the same reason as above, because we didn’t have a good way to describe the type of rest when obj is generic, we didn’t support this for a while.

Here we also considered a new rest operator, but we saw we already had the facilities for describing the above: our Pick and Exclude helper types in lib.d.ts To reiterate, ...rest basically picks off all of the properties on obj except for x, y, and z in the following example:

interface XYZ { x: any; y: any; z: any; }

function dropXYZ<T extends XYZ>(obj: T) {
    let { x, y, z, ...rest } = obj;
    return rest;
}

 

If we want to consider the properties of T (i.e. keyof T) except for x, y, and z, we can write Exclude<keyof T, "x" | "y" | "z">. We then want to pick those properties back off of the original type T, which gives us

Pick<T, Exclude<keyof T, "x" | "y" | "z">>`.

While it’s not the most beautiful type (hey, I’m no George Clooney myself), we can wrap it in a helper type like DropXYZ:

interface XYZ { x: any; y: any; z: any; }

type DropXYZ<T> = Pick<T, Exclude<keyof T, keyof XYZ>>;

function dropXYZ<T extends XYZ>(obj: T): DropXYZ<T> {
    let { x, y, z, ...rest } = obj;
    return rest;
}

 

Configuration inheritance via node_modules packages

For a long time TypeScript has supported extending tsconfig.json files using the extends field.

{
    "extends": "../tsconfig-base.json",
    "include": ["./**/*"]
    "compilerOptions": {
        // Override certain options on a project-by-project basis.
        "strictBindCallApply": false,
    }
}

This feature is very useful to avoid duplicating configuration which can easiy fall our of sync, but it really works best when multiple projects are co-located in the same respository so that each project can reference a common “base” tsconfig.json.

But for some teams, projects are written and published as completely independent packages. Those projects don’t have a common file they can reference, so as a workaround, users could create a separate package and reference that:

{
    "extends": "../node_modules/@my-team/tsconfig-base/tsconfig.json",
    "include": ["./**/*"]
    "compilerOptions": {
        // Override certain options on a project-by-project basis.
        "strictBindCallApply": false,
    }
}

However, climbing up parent directories with a series of leading ../s and reaching directly into node_modules to grab a specific file feels unwieldy.

TypeScript 3.2 now resolves tsconfig.jsons from node_modules. When using a bare path for the "extends" field in tsconfig.json, TypeScript will dive into node_modules packages for us.

{
    "extends": "@my-team/tsconfig-base",
    "include": ["./**/*"]
    "compilerOptions": {
        // Override certain options on a project-by-project basis.
        "strictBindCallApply": false,
    }
}

Here, TypeScript will climb up node_modules folders looking for a @my-team/tsconfig-base package. For each of those packages, TypeScript will first check whether package.json contains a "tsconfig" field, and if it does, TypeScript will try to load a configuration file from that field. If neither exists, TypeScript will try to read from a tsconfig.json at the root. This is similar to the lookup process for .js files in packages that Node uses, and the .d.ts lookup process that TypeScript already uses.

This feature can be extremely useful for bigger organizations, or projects with lots of distributed dependencies.

Diagnosing tsconfig.json with --showConfig

tsc, the TypeScript compiler, supports a new flag called --showConfig. When running tsc --showConfig, TypeScript will calculate the effective tsconfig.json (after calculating options inherited from the extends field) and print that out. This can be useful for diagnosing configuration issues in general.

BigInt

BigInts are part of an upcoming proposal in ECMAScript that allow us to model theoretically arbitrarily large integers. TypeScript 3.2 brings type-checking for BigInts, as well as support for emitting BigInt literals when targeting esnext.

BigInt support in TypeScript introduces a new primitive type called the bigint (all lowercase). You can get a bigint by calling the BigInt() function or by writing out a BigInt literal by adding an n to the end of any integer numeric literal:

let foo: bigint = BigInt(100); // the BigInt function
let bar: bigint = 100n;        // a BigInt literal

// *Slaps roof of fibonacci function*
// This bad boy returns ints that can get *so* big!
function fibonacci(n: bigint) {
    let result = 1n;
    for (let last = 0n, i = 0n; i < n; i++) {
        const current = result;
        result += last;
        last = current;
    }
    return result;
}

fibonacci(10000n)

 

While you might imagine close interaction between number and bigint, the two are separate domains.

declare let foo: number;
declare let bar: bigint;

foo = bar; // error: Type 'bigint' is not assignable to type 'number'.
bar = foo; // error: Type 'number' is not assignable to type 'bigint'.
As specified in ECMAScript, mixing numbers and bigints in arithmetic operations is an error. You'll have to explicitly convert values to BigInts.

console.log(3.141592 * 10000n);     // error
console.log(3145 * 10n);            // error
console.log(BigInt(3145) * 10n);    // okay!

 

Also important to note is that bigints produce a new string when using the typeof operator: the string "bigint". Thus, TypeScript correctly narrows using typeof as you’d expect.

function whatKindOfNumberIsIt(x: number | bigint) {
    if (typeof x === "bigint") {
        console.log("'x' is a bigint!");
    }
    else {
        console.log("'x' is a floating-point number");
    }
}

 

We’d like to extend a huge thanks to Caleb Sander for all the work on this feature. We’re grateful for the contribution, and we’re sure our users are too!

Caveats

As we mentioned, BigInt support is only available for the esnext target. It may not be obvious, but because BigInts have different behavior for mathematical operators like +, -, *, etc., providing functionality for older targets where the feature doesn’t exist (like es2017 and below) would involve rewriting each of these operations. TypeScript would need to dispatch to the correct behavior depending on the type, and so every addition, string concatenation, multiplication, etc. would involve a function call.

For that reason, we have no immediate plans to provide downleveling support. On the bright side, Node 11 and newer versions of Chrome already support this feature, so you’ll be able to use BigInts there when targeting esnext.

Certain targets may include a polyfill or BigInt-like runtime object. For those purposes you may want to add esnext.bigint to the lib setting in your compiler options.

Object.defineProperty declarations in JavaScript

When writing in JavaScript files (using allowJs), TypeScript now recognizes declarations that use Object.defineProperty. This means you’ll get better completions, and stronger type-checking when enabling type-checking in JavaScript files (by turning on the checkJs option or adding a // @ts-check comment to the top of your file).

// @ts-check

let obj = {};
Object.defineProperty(obj, "x", { value: "hello", writable: false });

obj.x.toLowercase();
//    ~~~~~~~~~~~
//    error:
//     Property 'toLowercase' does not exist on type 'string'.
//     Did you mean 'toLowerCase'?

obj.x = "world";
//  ~
//  error:
//   Cannot assign to 'x' because it is a read-only property.

 

Error message improvements

We’re continuing to push improvements in the error experience in TypeScript. Here’s a few things in TypeScript 3.2 that we believe will make the language easier to use.

Thanks to Kingwl, a-tarasyuk, and prateekgoel who helped out on some of these improvements.

Improved narrowing for tagged unions

TypeScript 3.2 makes narrowing easier by relaxing rules for what’s considered a discriminant property. Common properties of unions are now considered discriminants as long as they contain some singleton type (e.g. a string literal, null, or undefined), and they contain no generics.

As a result, TypeScript 3.2 considers the error property in the following example to be a discriminant, whereas before it wouldn’t since Error isn’t a singleton type. Thanks to this, narrowing works correctly in the body of the unwrap function.

type Either<T> =
    | { error: Error; data: null }
    | { error: null; data: T };

function unwrap<T>(result: Either<T>) {
    if (result.error) {
        // Here 'error' is non-null
        throw result.error;
    }

    // Now 'data' is non-null
    return result.data;
}

 

Editing improvements

The TypeScript project doesn’t simply consist of a compiler/type-checker. The core components of the compiler also provide a cross-platform open-source language service that can power “smarter” editor features like go-to-definition, find-all-references, and a number of quick fixes and refactorings. TypeScript 3.2 brings some small quality of life improvements.

Quick fixes

Implicit any suggestions and “infer from usage” fixes

We strongly suggest users take advantage of stricter checking when possible. noImplicitAny is one of these stricter checking modes, and it helps ensure that your code is as fully typed as possible which also leads to a better editing experience.

Unfortunately it’s not all roses for existing codebases. noImplicitAny is a big switch across codebases which can lead to a lot of error messages and red squiggles in your editor as you type code. The experience can be jarring to turn on just to find out which variables need types.

In this release, TypeScript produces suggestions for most variables and parameters that would have been reported as having implicit any types. When an editor reports these suggestions, TypeScript also provides a quick fix to automatically infer the types for you.

Types automatically filled in for implicit any parameters.

This can make migrating an existing codebase to TypeScript even easier, and we expect it will make migrating to noImplicitAny a breeze.

Going a step further, TypeScript users who are type-checking their .js files using checkJs or the // @ts-check comments can now also get the same functionality with JSDoc types!

Automatically generating JSDoc types for implicit any parameters in JavaScript files.

Other fixes

TypeScript 3.2 also brings two smaller quick fixes for small mistakes.

  • Add a missing new when accidentally calling a constructor.
  • Add an intermediate assertion to unknown when types are sufficiently unrelated.

Thanks to GitHub users iliashkolyar and ryanclarke respectively for these changes!

Improved formatting

Thanks to saschanaz, TypeScript is now smarter about formatting several different constructs. Listing all of them might be a bit cumbersome, but you can take a look at the pull request here.

Breaking changes and deprecations

lib.d.ts changes

TypeScript has recently moved more to generating DOM declarations in lib.d.ts by leveraging IDL files provided by standards groups. Upgraders should note take note of any issues they encounter related to the DOM and report them.

More specific types

Certain parameters no longer accept null, or now accept more specific types as per the corresponding specifications that describe the DOM.

More platform-specific deprecations

Certain properties that are WebKit-specific have been deprecated. They are likely to be removed in a new version.

wheelDelta and friends have been removed.

wheelDeltaX, wheelDelta, and wheelDeltaZ have all been removed as they are deprecated properties on WheelEvents.

As a solution, you can use deltaX, deltaY, and deltaZ instead. If older runtimes are a concern, you can include a file called legacy.d.ts in your project and write the following in it:

// legacy.d.ts

interface WheelEvent {
     readonly wheelDelta: number;
     readonly wheelDeltaX: number;
     readonly wheelDeltaZ: number;
}

 

JSX resolution changes

Our logic for resolving JSX invocations has been unified with our logic for resolving function calls. While this has simplified the compiler codebase and improved some use-cases, there may be some differences which we may need to reconcile. These changes are likely unintentional so they are not breaking changes per se, but upgraders should note take note of any issues they encounter and report them.

A note for NuGet and Visual Studio 2015

We have some changes coming in TypeScript 3.2 for NuGet and VS2015 users.

First, TypeScript 3.2 and future releases will only ship an MSBuild package, and not a standalone compiler package. Second, while our NuGet packages previously shipped with the Chakra JavaScript engine to run the compiler, the MSBuild package now depends on an invokable version of Node.js to be present. While machines with newer versions of Visual Studio 2017 (versions 15.8 and above) will not be impacted, some testing/CI machines, users with Visual Studio 2015, and users of Visual Studio 2017 15.7 and below may need to install Node.js directly from the site, through Visual Studio 2017 Build Tools (read more here), or via a redistribution of Node.js over NuGet. Otherwise, upgrading to TypeScript 3.2 might result in a build error like the following:

The build task could not find node.exe which is required to run the TypeScript compiler. Please install Node and ensure that the system path contains its location.

Lastly, TypeScript 3.2 will be the last TypeScript release with editor support for Visual Studio 2015 users. To stay current with TypeScript, we recommend upgrading to Visual Studio 2017 for the latest editing experience.

What’s next

Our next release of TypeScript is slated for the end of January. Some things we’ve got planned on the horizon are partial type argument inference and a quick fix to scaffold out declaration files that don’t exist on DefinitelyTyped. While this list is in flux, you can keep track of our plans on the TypeScript Roadmap.

We hope that TypeScript 3.2 makes your day-to-day coding more enjoyable, whether it comes to expressivity, productivity, or ease-of-use. If you’re enjoying it, drop us a line on Twitter at @typescriptlang; and if you’ve got ideas on what we should improve, file an issue on GitHub.

Happy hacking!

– Daniel Rosenwasser and the TypeScript team

2 comments

Discussion is closed. Login to edit/delete existing comments.

  • Ken Snyder 0

    Next release end of January? I’m guessing this is a typo.

    • Warren R 0

      Not a typo — this appears to be a complete republish of a blog post from November.

Feedback usabilla icon