Was this page helpful?

Overview

This overview page contains a shortened version of all the release notes for TypeScript. Because this page is so big. code samples have their interactive elements disabled.

TypeScript 4.2

Smarter Type Alias Preservation

TypeScript has a way to declare new names for types called type aliases. If you’re writing a set of functions that all work on string | number | boolean, you can write a type alias to avoid repeating yourself over and over again.

ts
type BasicPrimitive = number | string | boolean;

In TypeScript 4.2, our internals are a little smarter. We keep track of how types were constructed by keeping around parts of how they were originally written and constructed over time. We also keep track of, and differentiate, type aliases to instances of other aliases!

Being able to print back the types based on how you used them in your code means that as a TypeScript user, you can avoid some unfortunately humongous types getting displayed, and that often translates to getting better .d.ts file output, error messages, and in-editor type displays in quick info and signature help. This can help TypeScript feel a little bit more approachable for newcomers.

For more information, check out the first pull request that improves various cases around preserving union type aliases, along with a second pull request that preserves indirect aliases.

Leading/Middle Rest Elements in Tuple Types

In TypeScript, tuple types are meant to model arrays with specific lengths and element types.

ts
// A tuple that stores a pair of numbers
let a: [number, number] = [1, 2];
// A tuple that stores a string, a number, and a boolean
let b: [string, number, boolean] = ["hello", 42, true];

In TypeScript 4.2, rest elements specifically been expanded in how they can be used. In prior versions, TypeScript only allowed ...rest elements at the very last position of a tuple type.

However, now rest elements can occur anywhere within a tuple - with only a few restrictions.

ts
let foo: [...string[], number];
foo = [123];
foo = ["hello", 123];
foo = ["hello!", "hello!", "hello!", 123];
let bar: [boolean, ...string[], boolean];
bar = [true, false];
bar = [true, "some text", false];
bar = [true, "some", "separated", "text", false];

Even though JavaScript doesn’t have any syntax to model leading rest parameters, we were still able to declare doStuff as a function that takes leading arguments by declaring the ...args rest parameter with a tuple type that uses a leading rest element. This can help model lots of existing JavaScript out there!

For more details, see the original pull request.

Stricter Checks For The in Operator

In JavaScript, it is a runtime error to use a non-object type on the right side of the in operator. TypeScript 4.2 ensures this can be caught at design-time.

ts
// @errors: 2361
"foo" in 42;

This check is fairly conservative for the most part, so if you have received an error about this, it is likely an issue in the code.

A big thanks to our external contributor Jonas Hübotter for their pull request!

--noPropertyAccessFromIndexSignature

Back when TypeScript first introduced index signatures, you could only get properties declared by them with “bracketed” element access syntax like person["name"].

ts
interface SomeType {
/** This is an index signature. */
[propName: string]: any;
}
function doStuff(value: SomeType) {
let x = value["someProperty"];
}

This ended up being cumbersome in situations where we need to work with objects that have arbitrary properties. For example, imagine an API where it’s common to misspell a property name by adding an extra s character at the end.

ts
interface Options {
/** File patterns to be excluded. */
exclude?: string[];
/**
* It handles any extra properties that we haven't declared as type 'any'.
*/
[x: string]: any;
}
function processOptions(opts: Options) {
// Notice we're *intentionally* accessing `excludes`, not `exclude`
if (opts.excludes) {
console.error(
"The option `excludes` is not valid. Did you mean `exclude`?"
);
}
}

To make these types of situations easier, a while back, TypeScript made it possible to use “dotted” property access syntax like person.name when a type had a string index signature. This also made it easier to transition existing JavaScript code over to TypeScript.

In some cases, users would prefer to explicitly opt into the index signature - they would prefer to get an error message when a dotted property access doesn’t correspond to a specific property declaration.

That’s why TypeScript introduces a new flag called --noPropertyAccessFromIndexSignature. Under this mode, you’ll be opted in to TypeScript’s older behavior that issues an error. This new setting is not under the strict family of flags, since we believe users will find it more useful on certain codebases than others.

You can understand this feature in more detail by reading up on the corresponding pull request. We’d also like to extend a big thanks to Wenlu Wang who sent us this pull request!

abstract Construct Signatures

TypeScript allows us to mark a class as abstract. This tells TypeScript that the class is only meant to be extended from, and that certain members need to be filled in by any subclass to actually create an instance.

TypeScript 4.2 allows you to specify an abstract modifier on constructor signatures.

ts
abstract class Shape {
abstract getArea(): number;
}
// ---cut---
interface HasArea {
getArea(): number;
}
// Works!
let Ctor: abstract new () => HasArea = Shape;

Adding the abstract modifier to a construct signature signals that you can pass in abstract constructors. It doesn’t stop you from passing in other classes/constructor functions that are “concrete” - it really just signals that there’s no intent to run the constructor directly, so it’s safe to pass in either class type.

This feature allows us to write mixin factories in a way that supports abstract classes. For example, in the following code snippet, we’re able to use the mixin function withStyles with the abstract class SuperClass.

ts
abstract class SuperClass {
abstract someMethod(): void;
badda() {}
}
type AbstractConstructor<T> = abstract new (...args: any[]) => T
function withStyles<T extends AbstractConstructor<object>>(Ctor: T) {
abstract class StyledClass extends Ctor {
getStyles() {
// ...
}
}
return StyledClass;
}
class SubClass extends withStyles(SuperClass) {
someMethod() {
this.someMethod()
}
}

Note that withStyles is demonstrating a specific rule, where a class (like StyledClass) that extends a value that’s generic and bounded by an abstract constructor (like Ctor) has to also be declared abstract. This is because there’s no way to know if a class with more abstract members was passed in, and so it’s impossible to know whether the subclass implements all the abstract members.

You can read up more on abstract construct signatures on its pull request.

Understanding Your Project Structure With --explainFiles

A surprisingly common scenario for TypeScript users is to ask “why is TypeScript including this file?“. Inferring the files of your program turns out to be a complicated process, and so there are lots of reasons why a specific combination of lib.d.ts got used, why certain files in node_modules are getting included, and why certain files are being included even though we thought specifying exclude would keep them out.

That’s why TypeScript now provides an --explainFiles flag.

sh
tsc --explainFiles

When using this option, the TypeScript compiler will give some very verbose output about why a file ended up in your program. To read it more easily, you can forward the output to a file, or pipe it to a program that can easily view it.

sh
# Forward output to a text file
tsc --explainFiles > expanation.txt
# Pipe output to a utility program like `less`, or an editor like VS Code
tsc --explainFiles | less
tsc --explainFiles | code -

Typically, the output will start out by listing out reasons for including lib.d.ts files, then for local files, and then node_modules files.

TS_Compiler_Directory/4.2.2/lib/lib.es5.d.ts Library referenced via 'es5' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2015.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.es2015.d.ts Library referenced via 'es2015' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2016.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.es2016.d.ts Library referenced via 'es2016' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2017.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.es2017.d.ts Library referenced via 'es2017' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2018.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.es2018.d.ts Library referenced via 'es2018' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2019.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.es2019.d.ts Library referenced via 'es2019' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2020.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.es2020.d.ts Library referenced via 'es2020' from file 'TS_Compiler_Directory/4.2.2/lib/lib.esnext.d.ts' TS_Compiler_Directory/4.2.2/lib/lib.esnext.d.ts Library 'lib.esnext.d.ts' specified in compilerOptions ... More Library References... foo.ts Matched by include pattern '**/*' in 'tsconfig.json'

Right now, we make no guarantees about the output format - it might change over time. On that note, we’re interested in improving this format if you have any suggestions!

For more information, check out the original pull request!

Improved Uncalled Function Checks in Logical Expressions

Thanks to further improvements from Alex Tarasyuk, TypeScript’s uncalled function checks now apply within && and || expressions.

Under --strictNullChecks, the following code will now error.

ts
function shouldDisplayElement(element: Element) {
// ...
return true;
}
function getVisibleItems(elements: Element[]) {
return elements.filter((e) => shouldDisplayElement && e.children.length);
// ~~~~~~~~~~~~~~~~~~~~
// This condition will always return true since the function is always defined.
// Did you mean to call it instead.
}

For more details, check out the pull request here.

Destructured Variables Can Be Explicitly Marked as Unused

Thanks to another pull request from Alex Tarasyuk, you can now mark destructured variables as unused by prefixing them with an underscore (the _ character).

ts
let [_first, second] = getValues();

Previously, if _first was never used later on, TypeScript would issue an error under noUnusedLocals. Now, TypeScript will recognize that _first was intentionally named with an underscore because there was no intent to use it.

For more details, take a look at the full change.

Relaxed Rules Between Optional Properties and String Index Signatures

String index signatures are a way of typing dictionary-like objects, where you want to allow access with arbitrary keys:

ts
const movieWatchCount: { [key: string]: number } = {};
function watchMovie(title: string) {
movieWatchCount[title] = (movieWatchCount[title] ?? 0) + 1;
}

Of course, for any movie title not yet in the dictionary, movieWatchCount[title] will be undefined (TypeScript 4.1 added the option --noUncheckedIndexedAccess to include undefined when reading from an index signature like this). Even though it’s clear that there must be some strings not present in movieWatchCount, previous versions of TypeScript treated optional object properties as unassignable to otherwise compatible index signatures, due to the presence of undefined.

ts
type WesAndersonWatchCount = {
"Fantastic Mr. Fox"?: number;
"The Royal Tenenbaums"?: number;
"Moonrise Kingdom"?: number;
"The Grand Budapest Hotel"?: number;
};
declare const wesAndersonWatchCount: WesAndersonWatchCount;
const movieWatchCount: { [key: string]: number } = wesAndersonWatchCount;
// ~~~~~~~~~~~~~~~ error!
// Type 'WesAndersonWatchCount' is not assignable to type '{ [key: string]: number; }'.
// Property '"Fantastic Mr. Fox"' is incompatible with index signature.
// Type 'number | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'. (2322)

TypeScript 4.2 allows this assignment. However, it does not allow the assignment of non-optional properties with undefined in their types, nor does it allow writing undefined to a specific key:

ts
// @errors: 2322
type BatmanWatchCount = {
"Batman Begins": number | undefined;
"The Dark Knight": number | undefined;
"The Dark Knight Rises": number | undefined;
};
declare const batmanWatchCount: BatmanWatchCount;
// Still an error in TypeScript 4.2.
const movieWatchCount: { [key: string]: number } = batmanWatchCount;
// Still an error in TypeScript 4.2.
// Index signatures don't implicitly allow explicit `undefined`.
movieWatchCount["It's the Great Pumpkin, Charlie Brown"] = undefined;

The new rule also does not apply to number index signatures, since they are assumed to be array-like and dense:

ts
// @errors: 2322
declare let sortOfArrayish: { [key: number]: string };
declare let numberKeys: { 42?: string };
sortOfArrayish = numberKeys;

You can get a better sense of this change by reading up on the original PR.

Declare Missing Helper Function

Thanks to a community pull request from Alexander Tarasyuk, we now have a quick fix for declaring new functions and methods based on the call-site!

An un-declared function foo being called, with a quick fix scaffolding out the new contents of the file

Breaking Changes

We always strive to minimize breaking changes in a release. TypeScript 4.2 contains some breaking changes, but we believe they should be manageable in an upgrade.

lib.d.ts Updates

As with every TypeScript version, declarations for lib.d.ts (especially the declarations generated for web contexts), have changed. There are various changes, though Intl and ResizeObserver’s may end up being the most disruptive.

noImplicitAny Errors Apply to Loose yield Expressions

When the value of a yield expression is captured, but TypeScript can’t immediately figure out what type you intend for it to receive (i.e. the yield expression isn’t contextually typed), TypeScript will now issue an implicit any error.

ts
// @errors: 7057
function* g1() {
const value = yield 1;
}
function* g2() {
// No error.
// The result of `yield 1` is unused.
yield 1;
}
function* g3() {
// No error.
// `yield 1` is contextually typed by 'string'.
const value: string = yield 1;
}
function* g4(): Generator<number, void, string> {
// No error.
// TypeScript can figure out the type of `yield 1`
// from the explicit return type of `g3`.
const value = yield 1;
}

See more details in the corresponding changes.

Expanded Uncalled Function Checks

As described above, uncalled function checks will now operate consistently within && and || expressions when using --strictNullChecks. This can be a source of new breaks, but is typically an indication of a logic error in existing code.

Type Arguments in JavaScript Are Not Parsed as Type Arguments

Type arguments were already not allowed in JavaScript, but in TypeScript 4.2, the parser will parse them in a more spec-compliant way. So when writing the following code in a JavaScript file:

ts
f<T>(100);

TypeScript will parse it as the following JavaScript:

js
f < T > 100;

This may impact you if you were leveraging TypeScript’s API to parse type constructs in JavaScript files, which may have occurred when trying to parse Flow files.

See the pull request for more details on what’s checked.

Tuple size limits for spreads

Tuple types can be made by using any sort of spread syntax (...) in TypeScript.

ts
// Tuple types with spread elements
type NumStr = [number, string];
type NumStrNumStr = [...NumStr, ...NumStr];
// Array spread expressions
const numStr = [123, "hello"] as const;
const numStrNumStr = [...numStr, ...numStr] as const;

Sometimes these tuple types can accidentally grow to be huge, and that can make type-checking take a long time. Instead of letting the type-checking process hang (which is especially bad in editor scenarios), TypeScript has a limiter in place to avoid doing all that work.

You can see this pull request for more details.

.d.ts Extensions Cannot Be Used In Import Paths

In TypeScript 4.2, it is now an error for your import paths to contain .d.ts in the extension.

ts
// must be changed something like
// - "./foo"
// - "./foo.js"
import { Foo } from "./foo.d.ts";

Instead, your import paths should reflect whatever your loader will do at runtime. Any of the following imports might be usable instead.

ts
import { Foo } from "./foo";
import { Foo } from "./foo.js";
import { Foo } from "./foo/index.js";

Reverting Template Literal Inference

This change removed a feature from TypeScript 4.2 beta. If you haven’t yet upgraded past our last stable release, you won’t be affected, but you may still be interested in the change.

The beta version of TypeScript 4.2 included a change in inference to template strings. In this change, template string literals would either be given template string types or simplify to multiple string literal types. These types would then widen to string when assigning to mutable variables.

ts
declare const yourName: string;
// 'bar' is constant.
// It has type '`hello ${string}`'.
const bar = `hello ${yourName}`;
// 'baz' is mutable.
// It has type 'string'.
let baz = `hello ${yourName}`;

This is similar to how string literal inference works.

ts
// 'bar' has type '"hello"'.
const bar = "hello";
// 'baz' has type 'string'.
let baz = "hello";

For that reason, we believed that making template string expressions have template string types would be “consistent”; however, from what we’ve seen and heard, that isn’t always desirable.

In response, we’ve reverted this feature (and potential breaking change). If you do want a template string expression to be given a literal-like type, you can always add as const to the end of it.

ts
declare const yourName: string;
// 'bar' has type '`hello ${string}`'.
const bar = `hello ${yourName}` as const;
// ^^^^^^^^
// 'baz' has type 'string'.
const baz = `hello ${yourName}`;

TypeScript 4.1

Template Literal Types

String literal types in TypeScript allow us to model functions and APIs that expect a set of specific strings.

ts
// @errors: 2345
function setVerticalAlignment(color: "top" | "middle" | "bottom") {
// ...
}
setVerticalAlignment("middel");

This is pretty nice because string literal types can basically spell-check our string values.

We also like that string literals can be used as property names in mapped types. In this sense, they’re also usable as building blocks:

ts
type Options = {
[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean;
};
// same as
// type Options = {
// noImplicitAny?: boolean,
// strictNullChecks?: boolean,
// strictFunctionTypes?: boolean
// };

But there’s another place that that string literal types could be used as building blocks: building other string literal types.

That’s why TypeScript 4.1 brings the template literal string type. It has the same syntax as template literal strings in JavaScript, but is used in type positions. When you use it with concrete literal types, it produces a new string literal type by concatenating the contents.

ts
type World = "world";
type Greeting = `hello ${World}`;
// ^?

What happens when you have unions in substitution positions? It produces the set of every possible string literal that could be represented by each union member.

ts
type Color = "red" | "blue";
type Quantity = "one" | "two";
type SeussFish = `${Quantity | Color} fish`;
// ^?

This can be used beyond cute examples in release notes. For example, several libraries for UI components have a way to specify both vertical and horizontal alignment in their APIs, often with both at once using a single string like "bottom-right". Between vertically aligning with "top", "middle", and "bottom", and horizontally aligning with "left", "center", and "right", there are 9 possible strings where each of the former strings is connected with each of the latter strings using a dash.

ts
// @errors: 2345
type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";
// Takes
// | "top-left" | "top-center" | "top-right"
// | "middle-left" | "middle-center" | "middle-right"
// | "bottom-left" | "bottom-center" | "bottom-right"
declare function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void;
setAlignment("top-left"); // works!
setAlignment("top-middel"); // error!
setAlignment("top-pot"); // error! but good doughnuts if you're ever in Seattle

While there are lots of examples of this sort of API in the wild, this is still a bit of a toy example since we could write these out manually. In fact, for 9 strings, this is likely fine; but when you need a ton of strings, you should consider automatically generating them ahead of time to save work on every type-check (or just use string, which will be much simpler to comprehend).

Some of the real value comes from dynamically creating new string literals. For example, imagine a makeWatchedObject API that takes an object and produces a mostly identical object, but with a new on method to detect for changes to the properties.

ts
let person = makeWatchedObject({
firstName: "Homer",
age: 42, // give-or-take
location: "Springfield",
});
person.on("firstNameChanged", () => {
console.log(`firstName was changed!`);
});

Notice that on listens on the event "firstNameChanged", not just "firstName". How would we type this?

ts
type PropEventSource<T> = {
on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
};
/// Create a "watched object" with an 'on' method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;

With this, we can build something that errors when we give the wrong property!

ts
// @errors: 2345
type PropEventSource<T> = {
on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
};
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
let person = makeWatchedObject({
firstName: "Homer",
age: 42, // give-or-take
location: "Springfield",
});
// ---cut---
// error!
person.on("firstName", () => {});
// error!
person.on("frstNameChanged", () => {});

We can also do something special in template literal types: we can infer from substitution positions. We can make our last example generic to infer from parts of the eventName string to figure out the associated property.

ts
type PropEventSource<T> = {
on<K extends string & keyof T>
(eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void;
};
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
let person = makeWatchedObject({
firstName: "Homer",
age: 42,
location: "Springfield",
});
// works! 'newName' is typed as 'string'
person.on("firstNameChanged", newName => {
// 'newName' has the type of 'firstName'
console.log(`new name is ${newName.toUpperCase()}`);
});
// works! 'newAge' is typed as 'number'
person.on("ageChanged", newAge => {
if (newAge < 0) {
console.log("warning! negative age");
}
})

Here we made on into a generic method. When a user calls with the string "firstNameChanged', TypeScript will try to infer the right type for K. To do that, it will match K against the content prior to "Changed" and infer the string "firstName". Once TypeScript figures that out, the on method can fetch the type of firstName on the original object, which is string in this case. Similarly, when we call with "ageChanged", it finds the type for the property age which is number).

Inference can be combined in different ways, often to deconstruct strings, and reconstruct them in different ways. In fact, to help with modifying these string literal types, we’ve added a few new utility type aliases for modifying casing in letters (i.e. converting to lowercase and uppercase characters).

ts
type EnthusiasticGreeting<T extends string> = `${Uppercase<T>}`
type HELLO = EnthusiasticGreeting<"hello">;
// ^?

The new type aliases are Uppercase, Lowercase, Capitalize and Uncapitalize. The first two transform every character in a string, and the latter two transform only the first character in a string.

For more details, see the original pull request and the in-progress pull request to switch to type alias helpers.

Key Remapping in Mapped Types

Just as a refresher, a mapped type can create new object types based on arbitrary keys

ts
type Options = {
[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean;
};
// same as
// type Options = {
// noImplicitAny?: boolean,
// strictNullChecks?: boolean,
// strictFunctionTypes?: boolean
// };

or new object types based on other object types.

ts
/// 'Partial<T>' is the same as 'T', but with each property marked optional.
type Partial<T> = {
[K in keyof T]?: T[K];
};

Until now, mapped types could only produce new object types with keys that you provided them; however, lots of the time you want to be able to create new keys, or filter out keys, based on the inputs.

That’s why TypeScript 4.1 allows you to re-map keys in mapped types with a new as clause.

ts
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
// This is the new syntax!
}

With this new as clause, you can leverage features like template literal types to easily create property names based off of old ones.

ts
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
// ^?

and you can even filter out keys by producing never. That means you don’t have to use an extra Omit helper type in some cases.

ts
// Remove the 'kind' property
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// ^?

For more information, take a look at the original pull request over on GitHub.

Recursive Conditional Types

In JavaScript it’s fairly common to see functions that can flatten and build up container types at arbitrary levels. For example, consider the .then() method on instances of Promise. .then(...) unwraps each promise until it finds a value that’s not “promise-like”, and passes that value to a callback. There’s also a relatively new flat method on Arrays that can take a depth of how deep to flatten.

Expressing this in TypeScript’s type system was, for all practical intents and purposes, not possible. While there were hacks to achieve this, the types ended up looking very unreasonable.

That’s why TypeScript 4.1 eases some restrictions on conditional types - so that they can model these patterns. In TypeScript 4.1, conditional types can now immediately reference themselves within their branches, making it easier to write recursive type aliases.

For example, if we wanted to write a type to get the element types of nested arrays, we could write the following deepFlatten type.

ts
type ElementType<T> = T extends ReadonlyArray<infer U> ? ElementType<U> : T;
function deepFlatten<T extends readonly unknown[]>(x: T): ElementType<T>[] {
throw "not implemented";
}
// All of these return the type 'number[]':
deepFlatten([1, 2, 3]);
deepFlatten([[1], [2, 3]]);
deepFlatten([[1], [[2]], [[[3]]]]);

Similarly, in TypeScript 4.1 we can write an Awaited type to deeply unwrap Promises.

ts
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
/// Like `promise.then(...)`, but more accurate in types.
declare function customThen<T, U>(
p: Promise<T>,
onFulfilled: (value: Awaited<T>) => U
): Promise<Awaited<U>>;

Keep in mind that while these recursive types are powerful, but they should be used responsibly and sparingly.

First off, these types can do a lot of work which means that they can increase type-checking time. Trying to model numbers in the Collatz conjecture or Fibonacci sequence might be fun, but don’t ship that in .d.ts files on npm.

But apart from being computationally intensive, these types can hit an internal recursion depth limit on sufficiently-complex inputs. When that recursion limit is hit, that results in a compile-time error. In general, it’s better not to use these types at all than to write something that fails on more realistic examples.

See more at the implementation.

Checked Indexed Accesses (--noUncheckedIndexedAccess)

TypeScript has a feature called index signatures. These signatures are a way to signal to the type system that users can access arbitrarily-named properties.

ts
interface Options {
path: string;
permissions: number;
// Extra properties are caught by this index signature.
[propName: string]: string | number;
}
function checkOptions(opts: Options) {
opts.path; // string
opts.permissions; // number
// These are all allowed too!
// They have the type 'string | number'.
opts.yadda.toString();
opts["foo bar baz"].toString();
opts[Math.random()].toString();
}

In the above example, Options has an index signature that says any accessed property that’s not already listed should have the type string | number. This is often convenient for optimistic code that assumes you know what you’re doing, but the truth is that most values in JavaScript do not support every potential property name. Most types will not, for example, have a value for a property key created by Math.random() like in the previous example. For many users, this behavior was undesirable, and felt like it wasn’t leveraging the full strict-checking of --strictNullChecks.

That’s why TypeScript 4.1 ships with a new flag called --noUncheckedIndexedAccess. Under this new mode, every property access (like foo.bar) or indexed access (like foo["bar"]) is considered potentially undefined. That means that in our last example, opts.yadda will have the type string | number | undefined as opposed to just string | number. If you need to access that property, you’ll either have to check for its existence first or use a non-null assertion operator (the postfix ! character).

This flag can be handy for catching out-of-bounds errors, but it might be noisy for a lot of code, so it is not automatically enabled by the --strict flag; however, if this feature is interesting to you, you should feel free to try it and determine whether it makes sense for your team’s codebase!

You can learn more at the implementing pull request.

paths without baseUrl

Using path-mapping is fairly common - often it’s to have nicer imports, often it’s to simulate monorepo linking behavior.

Unfortunately, specifying paths to enable path-mapping required also specifying an option called baseUrl, which allows bare specifier paths to be reached relative to the baseUrl too. This also often caused poor paths to be used by auto-imports.

In TypeScript 4.1, the paths option can be used without baseUrl. This helps avoid some of these issues.

checkJs Implies allowJs

Previously if you were starting a checked JavaScript project, you had to set both allowJs and checkJs. This was a slightly annoying bit of friction in the experience, so checkJs now implies allowJs by default.

See more details at the pull request.

React 17 JSX Factories

TypeScript 4.1 supports React 17’s upcoming jsx and jsxs factory functions through two new options for the jsx compiler option:

  • react-jsx
  • react-jsxdev

These options are intended for production and development compiles respectively. Often, the options from one can extend from the other.

For more information, check out the corresponding PR.

TypeScript 4.0

Variadic Tuple Types

Consider a function in JavaScript called concat that takes two array or tuple types and concatenates them together to make a new array.

js
function concat(arr1, arr2) {
return [...arr1, ...arr2];
}

Also consider tail, that takes an array or tuple, and returns all elements but the first.

js
function tail(arg) {
const [_, ...result] = arg;
return result;
}

How would we type either of these in TypeScript?

For concat, the only valid thing we could do in older versions of the language was to try and write some overloads.

ts
function concat(arr1: [], arr2: []): [];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)

Uh…okay, that’s…seven overloads for when the second array is always empty. Let’s add some for when arr2 has one argument.

ts
function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];

We hope it’s clear that this is getting unreasonable. Unfortunately, you’d also end up with the same sorts of issues typing a function like tail.

This is another case of what we like to call “death by a thousand overloads”, and it doesn’t even solve the problem generally. It only gives correct types for as many overloads as we care to write. If we wanted to make a catch-all case, we’d need an overload like the following:

ts
function concat<T, U>(arr1: T[], arr2: U[]): Array<T | U>;

But that signature doesn’t encode anything about the lengths of the input, or the order of the elements, when using tuples.

TypeScript 4.0 brings two fundamental changes, along with inference improvements, to make typing these possible.

The first change is that spreads in tuple type syntax can now be generic. This means that we can represent higher-order operations on tuples and arrays even when we don’t know the actual types we’re operating over. When generic spreads are instantiated (or, replaced with a real type) in these tuple types, they can produce other sets of array and tuple types.

For example, that means we can type function like tail, without our “death by a thousand overloads” issue.

ts
function tail<T extends any[]>(arr: readonly [any, ...T]) {
const [_ignored, ...rest] = arr;
return rest;
}
const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];
const r1 = tail(myTuple);
// ^?
const r2 = tail([...myTuple, ...myArray] as const);
// ^?

The second change is that rest elements can occur anywhere in a tuple - not just at the end!

ts
type Strings = [string, string];
type Numbers = [number, number];
type StrStrNumNumBool = [...Strings, ...Numbers, boolean];

Previously, TypeScript would issue an error like the following:

A rest element must be last in a tuple type.

But with TypeScript 4.0, this restriction is relaxed.

Note that in cases when we spread in a type without a known length, the resulting type becomes unbounded as well, and all the following elements factor into the resulting rest element type.

ts
type Strings = [string, string];
type Numbers = number[];
type Unbounded = [...Strings, ...Numbers, boolean];

By combining both of these behaviors together, we can write a single well-typed signature for concat:

ts
type Arr = readonly any[];
function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
return [...arr1, ...arr2];
}

While that one signature is still a bit lengthy, it’s just one signature that doesn’t have to be repeated, and it gives predictable behavior on all arrays and tuples.

This functionality on its own is great, but it shines in more sophisticated scenarios too. For example, consider a function to partially apply arguments called partialCall. partialCall takes a function - let’s call it f - along with the initial few arguments that f expects. It then returns a new function that takes any other arguments that f still needs, and calls f when it receives them.

js
function partialCall(f, ...headArgs) {
return (...tailArgs) => f(...headArgs, ...tailArgs);
}

TypeScript 4.0 improves the inference process for rest parameters and rest tuple elements so that we can type this and have it “just work”.

ts
type Arr = readonly unknown[];
function partialCall<T extends Arr, U extends Arr, R>(
f: (...args: [...T, ...U]) => R,
...headArgs: T
) {
return (...tailArgs: U) => f(...headArgs, ...tailArgs);
}

In this case, partialCall understands which parameters it can and can’t initially take, and returns functions that appropriately accept and reject anything left over.

ts
// @errors: 2345 2554 2554 2345
type Arr = readonly unknown[];
function partialCall<T extends Arr, U extends Arr, R>(
f: (...args: [...T, ...U]) => R,
...headArgs: T
) {
return (...tailArgs: U) => f(...headArgs, ...tailArgs);
}
// ---cut---
const foo = (x: string, y: number, z: boolean) => {};
const f1 = partialCall(foo, 100);
const f2 = partialCall(foo, "hello", 100, true, "oops");
// This works!
const f3 = partialCall(foo, "hello");
// ^?
// What can we do with f3 now?
// Works!
f3(123, true);
f3();
f3(123, "hello");

Variadic tuple types enable a lot of new exciting patterns, especially around function composition. We expect we may be able to leverage it to do a better job type-checking JavaScript’s built-in bind method. A handful of other inference improvements and patterns also went into this, and if you’re interested in learning more, you can take a look at the pull request for variadic tuples.

Labeled Tuple Elements

Improving the experience around tuple types and parameter lists is important because it allows us to get strongly typed validation around common JavaScript idioms - really just slicing and dicing argument lists and passing them to other functions. The idea that we can use tuple types for rest parameters is one place where this is crucial.

For example, the following function that uses a tuple type as a rest parameter…

ts
function foo(...args: [string, number]): void {
// ...
}

…should appear no different from the following function…

ts
function foo(arg0: string, arg1: number): void {
// ...
}

…for any caller of foo.

ts
// @errors: 2554
function foo(arg0: string, arg1: number): void {
// ...
}
// ---cut---
foo("hello", 42);
foo("hello", 42, true);
foo("hello");

There is one place where the differences begin to become observable though: readability. In the first example, we have no parameter names for the first and second elements. While these have no impact on type-checking, the lack of labels on tuple positions can make them harder to use - harder to communicate our intent.

That’s why in TypeScript 4.0, tuples types can now provide labels.

ts
type Range = [start: number, end: number];

To deepen the connection between parameter lists and tuple types, the syntax for rest elements and optional elements mirrors the syntax for parameter lists.

ts
type Foo = [first: number, second?: string, ...rest: any[]];

There are a few rules when using labeled tuples. For one, when labeling a tuple element, all other elements in the tuple must also be labeled.

ts
// @errors: 5084
type Bar = [first: string, number];

It’s worth noting - labels don’t require us to name our variables differently when destructuring. They’re purely there for documentation and tooling.

ts
function foo(x: [first: string, second: number]) {
// ...
// note: we didn't need to name these 'first' and 'second'
const [a, b] = x;
a
// ^?
b
// ^?
}

Overall, labeled tuples are handy when taking advantage of patterns around tuples and argument lists, along with implementing overloads in a type-safe way. In fact, TypeScript’s editor support will try to display them as overloads when possible.

Signature help displaying a union of labeled tuples as in a parameter list as two signatures

To learn more, check out the pull request for labeled tuple elements.

Class Property Inference from Constructors

TypeScript 4.0 can now use control flow analysis to determine the types of properties in classes when noImplicitAny is enabled.

ts
class Square {
// Previously both of these were any
area;
// ^?
sideLength;
// ^?
constructor(sideLength: number) {
this.sideLength = sideLength;
this.area = sideLength ** 2;
}
}

In cases where not all paths of a constructor assign to an instance member, the property is considered to potentially be undefined.

ts
// @errors: 2532
class Square {
sideLength;
// ^?
constructor(sideLength: number) {
if (Math.random()) {
this.sideLength = sideLength;
}
}
get area() {
return this.sideLength ** 2;
}
}

In cases where you know better (e.g. you have an initialize method of some sort), you’ll still need an explicit type annotation along with a definite assignment assertion (!) if you’re in strictPropertyInitialization.

ts
class Square {
// definite assignment assertion
// v
sideLength!: number;
// ^^^^^^^^
// type annotation
constructor(sideLength: number) {
this.initialize(sideLength);
}
initialize(sideLength: number) {
this.sideLength = sideLength;
}
get area() {
return this.sideLength ** 2;
}
}

For more details, see the implementing pull request.

Short-Circuiting Assignment Operators

JavaScript, and a lot of other languages, support a set of operators called compound assignment operators. Compound assignment operators apply an operator to two arguments, and then assign the result to the left side. You may have seen these before:

ts
// Addition
// a = a + b
a += b;
// Subtraction
// a = a - b
a -= b;
// Multiplication
// a = a * b
a *= b;
// Division
// a = a / b
a /= b;
// Exponentiation
// a = a ** b
a **= b;
// Left Bit Shift
// a = a << b
a <<= b;

So many operators in JavaScript have a corresponding assignment operator! Up until recently, however, there were three notable exceptions: logical and (&&), logical or (||), and nullish coalescing (??).

That’s why TypeScript 4.0 supports a new ECMAScript feature to add three new assignment operators: &&=, ||=, and ??=.

These operators are great for substituting any example where a user might write code like the following:

ts
a = a && b;
a = a || b;
a = a ?? b;

Or a similar if block like

ts
// could be 'a ||= b'
if (!a) {
a = b;
}

There are even some patterns we’ve seen (or, uh, written ourselves) to lazily initialize values, only if they’ll be needed.

ts
let values: string[];
(values ?? (values = [])).push("hello");
// After
(values ??= []).push("hello");

(look, we’re not proud of all the code we write…)

On the rare case that you use getters or setters with side-effects, it’s worth noting that these operators only perform assignments if necessary. In that sense, not only is the right side of the operator “short-circuited” - the assignment itself is too.

ts
obj.prop ||= foo();
// roughly equivalent to either of the following
obj.prop || (obj.prop = foo());
if (!obj.prop) {
obj.prop = foo();
}

Try running the following example to see how that differs from always performing the assignment.

ts
const obj = {
get prop() {
console.log("getter has run");
// Replace me!
return Math.random() < 0.5;
},
set prop(_val: boolean) {
console.log("setter has run");
}
};
function foo() {
console.log("right side evaluated");
return true;
}
console.log("This one always runs the setter");
obj.prop = obj.prop || foo();
console.log("This one *sometimes* runs the setter");
obj.prop ||= foo();

We’d like to extend a big thanks to community member Wenlu Wang for this contribution!

For more details, you can take a look at the pull request here. You can also check out TC39’s proposal repository for this feature.

unknown on catch Clause Bindings

Since the beginning days of TypeScript, catch clause variables have always been typed as any. This meant that TypeScript allowed you to do anything you wanted with them.

ts
try {
// Do some work
} catch (x) {
// x has type 'any' - have fun!
console.log(x.message);
console.log(x.toUpperCase());
x++;
x.yadda.yadda.yadda();
}

The above has some undesirable behavior if we’re trying to prevent more errors from happening in our error-handling code! Because these variables have the type any by default, they lack any type-safety which could have errored on invalid operations.

That’s why TypeScript 4.0 now lets you specify the type of catch clause variables as unknown instead. unknown is safer than any because it reminds us that we need to perform some sorts of type-checks before operating on our values.

ts
// @errors: 2571
try {
// ...
} catch (e: unknown) {
// Can't access values on unknowns
console.log(e.toUpperCase());
if (typeof e === "string") {
// We've narrowed 'e' down to the type 'string'.
console.log(e.toUpperCase());
}
}

While the types of catch variables won’t change by default, we might consider a new --strict mode flag in the future so that users can opt in to this behavior. In the meantime, it should be possible to write a lint rule to force catch variables to have an explicit annotation of either : any or : unknown.

For more details you can peek at the changes for this feature.

Custom JSX Factories

When using JSX, a fragment is a type of JSX element that allows us to return multiple child elements. When we first implemented fragments in TypeScript, we didn’t have a great idea about how other libraries would utilize them. Nowadays most other libraries that encourage using JSX and support fragments have a similar API shape.

In TypeScript 4.0, users can customize the fragment factory through the new jsxFragmentFactory option.

As an example, the following tsconfig.json file tells TypeScript to transform JSX in a way compatible with React, but switches each factory invocation to h instead of React.createElement, and uses Fragment instead of React.Fragment.

{
"": "esnext",
"": "commonjs",
"": "react",
"": "h",
"": "Fragment"
}
}

In cases where you need to have a different JSX factory on a per-file basis, you can take advantage of the new /** @jsxFrag */ pragma comment. For example, the following…

tsx
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
 
/** @jsx h */
/** @jsxFrag Fragment */
 
import { h, Fragment } from "preact";
 
export const Header = (
<>
<h1>Welcome</h1>
</>
);

…will get transformed to this output JavaScript…

tsx
import React from 'react';
export const Header = (React.createElement(React.Fragment, null,
React.createElement("h1", null, "Welcome")));
 

We’d like to extend a big thanks to community member Noj Vek for sending this pull request and patiently working with our team on it.

You can see that the pull request for more details!

TypeScript 3.9

Improvements in Inference and Promise.all

Recent versions of TypeScript (around 3.7) have had updates to the declarations of functions like Promise.all and Promise.race. Unfortunately, that introduced a few regressions, especially when mixing in values with null or undefined.

ts
interface Lion {
roar(): void;
}
interface Seal {
singKissFromARose(): void;
}
async function visitZoo(
lionExhibit: Promise<Lion>,
sealExhibit: Promise<Seal | undefined>
) {
let [lion, seal] = await Promise.all([lionExhibit, sealExhibit]);
lion.roar(); // uh oh
// ~~~~
// Object is possibly 'undefined'.
}

This is strange behavior! The fact that sealExhibit contained an undefined somehow poisoned type of lion to include undefined.

Thanks to a pull request from Jack Bates, this has been fixed with improvements in our inference process in TypeScript 3.9. The above no longer errors. If you’ve been stuck on older versions of TypeScript due to issues around Promises, we encourage you to give 3.9 a shot!

What About the awaited Type?

If you’ve been following our issue tracker and design meeting notes, you might be aware of some work around a new type operator called awaited. This goal of this type operator is to accurately model the way that Promise unwrapping works in JavaScript.

We initially anticipated shipping awaited in TypeScript 3.9, but as we’ve run early TypeScript builds with existing codebases, we’ve realized that the feature needs more design work before we can roll it out to everyone smoothly. As a result, we’ve decided to pull the feature out of our main branch until we feel more confident. We’ll be experimenting more with the feature, but we won’t be shipping it as part of this release.

Speed Improvements

TypeScript 3.9 ships with many new speed improvements. Our team has been focusing on performance after observing extremely poor editing/compilation speed with packages like material-ui and styled-components. We’ve dived deep here, with a series of different pull requests that optimize certain pathological cases involving large unions, intersections, conditional types, and mapped types.

Each of these pull requests gains about a 5-10% reduction in compile times on certain codebases. In total, we believe we’ve achieved around a 40% reduction in material-ui’s compile time!

We also have some changes to file renaming functionality in editor scenarios. We heard from the Visual Studio Code team that when renaming a file, just figuring out which import statements needed to be updated could take between 5 to 10 seconds. TypeScript 3.9 addresses this issue by changing the internals of how the compiler and language service caches file lookups.

While there’s still room for improvement, we hope this work translates to a snappier experience for everyone!

// @ts-expect-error Comments

Imagine that we’re writing a library in TypeScript and we’re exporting some function called doStuff as part of our public API. The function’s types declare that it takes two strings so that other TypeScript users can get type-checking errors, but it also does a runtime check (maybe only in development builds) to give JavaScript users a helpful error.

ts
function doStuff(abc: string, xyz: string) {
assert(typeof abc === "string");
assert(typeof xyz === "string");
// do some stuff
}

So TypeScript users will get a helpful red squiggle and an error message when they misuse this function, and JavaScript users will get an assertion error. We’d like to test this behavior, so we’ll write a unit test.

ts
expect(() => {
doStuff(123, 456);
}).toThrow();

Unfortunately if our tests are written in TypeScript, TypeScript will give us an error!

ts
doStuff(123, 456);
// ~~~
// error: Type 'number' is not assignable to type 'string'.

That’s why TypeScript 3.9 brings a new feature: // @ts-expect-error comments. When a line is prefixed with a // @ts-expect-error comment, TypeScript will suppress that error from being reported; but if there’s no error, TypeScript will report that // @ts-expect-error wasn’t necessary.

As a quick example, the following code is okay

ts
// @ts-expect-error
console.log(47 * "octopus");

while the following code

ts
// @ts-expect-error
console.log(1 + 1);

results in the error

Unused '@ts-expect-error' directive.

We’d like to extend a big thanks to Josh Goldberg, the contributor who implemented this feature. For more information, you can take a look at the ts-expect-error pull request.

ts-ignore or ts-expect-error?

In some ways // @ts-expect-error can act as a suppression comment, similar to // @ts-ignore. The difference is that // @ts-ignore will do nothing if the following line is error-free.

You might be tempted to switch existing // @ts-ignore comments over to // @ts-expect-error, and you might be wondering which is appropriate for future code. While it’s entirely up to you and your team, we have some ideas of which to pick in certain situations.

Pick ts-expect-error if:

  • you’re writing test code where you actually want the type system to error on an operation
  • you expect a fix to be coming in fairly quickly and you just need a quick workaround
  • you’re in a reasonably-sized project with a proactive team that wants to remove suppression comments as soon affected code is valid again

Pick ts-ignore if:

  • you have an a larger project and and new errors have appeared in code with no clear owner
  • you are in the middle of an upgrade between two different versions of TypeScript, and a line of code errors in one version but not another.
  • you honestly don’t have the time to decide which of these options is better.

Uncalled Function Checks in Conditional Expressions

In TypeScript 3.7 we introduced uncalled function checks to report an error when you’ve forgotten to call a function.

ts
function hasImportantPermissions(): boolean {
// ...
}
// Oops!
if (hasImportantPermissions) {
// ~~~~~~~~~~~~~~~~~~~~~~~
// This condition will always return true since the function is always defined.
// Did you mean to call it instead?
deleteAllTheImportantFiles();
}

However, this error only applied to conditions in if statements. Thanks to a pull request from Alexander Tarasyuk, this feature is also now supported in ternary conditionals (i.e. the cond ? trueExpr : falseExpr syntax).

ts
declare function listFilesOfDirectory(dirPath: string): string[];
declare function isDirectory(): boolean;
function getAllFiles(startFileName: string) {
const result: string[] = [];
traverse(startFileName);
return result;
function traverse(currentPath: string) {
return isDirectory
? // ~~~~~~~~~~~
// This condition will always return true
// since the function is always defined.
// Did you mean to call it instead?
listFilesOfDirectory(currentPath).forEach(traverse)
: result.push(currentPath);
}
}

https://github.com/microsoft/TypeScript/issues/36048

Editor Improvements

The TypeScript compiler not only powers the TypeScript editing experience in most major editors, it also powers the JavaScript experience in the Visual Studio family of editors and more. Using new TypeScript/JavaScript functionality in your editor will differ depending on your editor, but

CommonJS Auto-Imports in JavaScript

One great new improvement is in auto-imports in JavaScript files using CommonJS modules.

In older versions, TypeScript always assumed that regardless of your file, you wanted an ECMAScript-style import like

js
import * as fs from "fs";

However, not everyone is targeting ECMAScript-style modules when writing JavaScript files. Plenty of users still use CommonJS-style require(...) imports like so

js
const fs = require("fs");

TypeScript now automatically detects the types of imports you’re using to keep your file’s style clean and consistent.

For more details on the change, see the corresponding pull request.

Code Actions Preserve Newlines

TypeScript’s refactorings and quick fixes often didn’t do a great job of preserving newlines. As a really basic example, take the following code.

ts
const maxValue = 100;
/*start*/
for (let i = 0; i <= maxValue; i++) {
// First get the squared value.
let square = i ** 2;
// Now print the squared value.
console.log(square);
}
/*end*/

If we highlighted the range from /*start*/ to /*end*/ in our editor to extract to a new function, we’d end up with code like the following.

ts
const maxValue = 100;
printSquares();
function printSquares() {
for (let i = 0; i <= maxValue; i++) {
// First get the squared value.
let square = i ** 2;
// Now print the squared value.
console.log(square);
}
}

Extracting the for loop to a function in older versions of TypeScript. A newline is not preserved.

That’s not ideal - we had a blank line between each statement in our for loop, but the refactoring got rid of it! TypeScript 3.9 does a little more work to preserve what we write.

ts
const maxValue = 100;
printSquares();
function printSquares() {
for (let i = 0; i <= maxValue; i++) {
// First get the squared value.
let square = i ** 2;
// Now print the squared value.
console.log(square);
}
}

Extracting the for loop to a function in TypeScript 3.9. A newline is preserved.

You can see more about the implementation in this pull request

Quick Fixes for Missing Return Expressions

There are occasions where we might forget to return the value of the last statement in a function, especially when adding curly braces to arrow functions.

ts
// before
let f1 = () => 42;
// oops - not the same!
let f2 = () => {
42;
};

Thanks to a pull request from community member Wenlu Wang, TypeScript can provide a quick-fix to add missing return statements, remove curly braces, or add parentheses to arrow function bodies that look suspiciously like object literals.

TypeScript fixing an error where no expression is returned by adding a return statement or removing curly braces.

Support for “Solution Style” tsconfig.json Files

Editors need to figure out which configuration file a file belongs to so that it can apply the appropriate options and figure out which other files are included in the current “project”. By default, editors powered by TypeScript’s language server do this by walking up each parent directory to find a tsconfig.json.

One case where this slightly fell over is when a tsconfig.json simply existed to reference other tsconfig.json files.

// tsconfig.json
{
"": [],
{ "path": "./tsconfig.shared.json" },
{ "path": "./tsconfig.frontend.json" },
{ "path": "./tsconfig.backend.json" }
]
}

This file that really does nothing but manage other project files is often called a “solution” in some environments. Here, none of these tsconfig.*.json files get picked up by the server, but we’d really like the language server to understand that the current .ts file probably belongs to one of the mentioned projects in this root tsconfig.json.

TypeScript 3.9 adds support to editing scenarios for this configuration. For more details, take a look at the pull request that added this functionality.

Breaking Changes

Parsing Differences in Optional Chaining and Non-Null Assertions

TypeScript recently implemented the optional chaining operator, but we’ve received user feedback that the behavior of optional chaining (?.) with the non-null assertion operator (!) is extremely counter-intuitive.

Specifically, in previous versions, the code

ts
foo?.bar!.baz;

was interpreted to be equivalent to the following JavaScript.

js
(foo?.bar).baz;

In the above code the parentheses stop the “short-circuiting” behavior of optional chaining, so if foo is undefined, accessing baz will cause a runtime error.

The Babel team who pointed this behavior out, and most users who provided feedback to us, believe that this behavior is wrong. We do too! The thing we heard the most was that the ! operator should just “disappear” since the intent was to remove null and undefined from the type of bar.

In other words, most people felt that the original snippet should be interpreted as

js
foo?.bar.baz;

which just evaluates to undefined when foo is undefined.

This is a breaking change, but we believe most code was written with the new interpretation in mind. Users who want to revert to the old behavior can add explicit parentheses around the left side of the ! operator.

ts
foo?.bar!.baz;

} and > are Now Invalid JSX Text Characters

The JSX Specification forbids the use of the } and > characters in text positions. TypeScript and Babel have both decided to enforce this rule to be more comformant. The new way to insert these characters is to use an HTML escape code (e.g. <span> 2 &gt 1 </div>) or insert an expression with a string literal (e.g. <span> 2 {">"} 1 </div>).

Luckily, thanks to the pull request enforcing this from Brad Zacher, you’ll get an error message along the lines of

Unexpected token. Did you mean `{'>'}` or `>`? Unexpected token. Did you mean `{'}'}` or `}`?

For example:

tsx
let directions = <span>Navigate to: Menu Bar > Tools > Options</div>
// ~ ~
// Unexpected token. Did you mean `{'>'}` or `>`?

That error message came with a handy quick fix, and thanks to Alexander Tarasyuk, you can apply these changes in bulk if you have a lot of errors.

Stricter Checks on Intersections and Optional Properties

Generally, an intersection type like A & B is assignable to C if either A or B is assignable to C; however, sometimes that has problems with optional properties. For example, take the following:

ts
interface A {
a: number; // notice this is 'number'
}
interface B {
b: string;
}
interface C {
a?: boolean; // notice this is 'boolean'
b: string;
}
declare let x: A & B;
declare let y: C;
y = x;

In previous versions of TypeScript, this was allowed because while A was totally incompatible with C, B was compatible with C.

In TypeScript 3.9, so long as every type in an intersection is a concrete object type, the type system will consider all of the properties at once. As a result, TypeScript will see that the a property of A & B is incompatible with that of C:

Type 'A & B' is not assignable to type 'C'. Types of property 'a' are incompatible. Type 'number' is not assignable to type 'boolean | undefined'.

For more information on this change, see the corresponding pull request.

Intersections Reduced By Discriminant Properties

There are a few cases where you might end up with types that describe values that just don’t exist. For example

ts
declare function smushObjects<T, U>(x: T, y: U): T & U;
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
declare let x: Circle;
declare let y: Square;
let z = smushObjects(x, y);
console.log(z.kind);

This code is slightly weird because there’s really no way to create an intersection of a Circle and a Square - they have two incompatible kind fields. In previous versions of TypeScript, this code was allowed and the type of kind itself was never because "circle" & "square" described a set of values that could never exist.

In TypeScript 3.9, the type system is more aggressive here - it notices that it’s impossible to intersect Circle and Square because of their kind properties. So instead of collapsing the type of z.kind to never, it collapses the type of z itself (Circle & Square) to never. That means the above code now errors with:

Property 'kind' does not exist on type 'never'.

Most of the breaks we observed seem to correspond with slightly incorrect type declarations. For more details, see the original pull request.

Getters/Setters are No Longer Enumerable

In older versions of TypeScript, get and set accessors in classes were emitted in a way that made them enumerable; however, this wasn’t compliant with the ECMAScript specification which states that they must be non-enumerable. As a result, TypeScript code that targeted ES5 and ES2015 could differ in behavior.

Thanks to a pull request from GitHub user pathurs, TypeScript 3.9 now conforms more closely with ECMAScript in this regard.

Type Parameters That Extend any No Longer Act as any

In previous versions of TypeScript, a type parameter constrained to any could be treated as any.

ts
function foo<T extends any>(arg: T) {
arg.spfjgerijghoied; // no error!
}

This was an oversight, so TypeScript 3.9 takes a more conservative approach and issues an error on these questionable operations.

ts
function foo<T extends any>(arg: T) {
arg.spfjgerijghoied;
// ~~~~~~~~~~~~~~~
// Property 'spfjgerijghoied' does not exist on type 'T'.
}

export * is Always Retained

In previous TypeScript versions, declarations like export * from "foo" would be dropped in our JavaScript output if foo didn’t export any values. This sort of emit is problematic because it’s type-directed and can’t be emulated by Babel. TypeScript 3.9 will always emit these export * declarations. In practice, we don’t expect this to break much existing code.

TypeScript 3.8

Type-Only Imports and Export

This feature is something most users may never have to think about; however, if you’ve hit issues under --isolatedModules, TypeScript’s transpileModule API, or Babel, this feature might be relevant.

TypeScript 3.8 adds a new syntax for type-only imports and exports.

ts
import type { SomeThing } from "./some-module.js";
export type { SomeThing };

import type only imports declarations to be used for type annotations and declarations. It always gets fully erased, so there’s no remnant of it at runtime. Similarly, export type only provides an export that can be used for type contexts, and is also erased from TypeScript’s output.

It’s important to note that classes have a value at runtime and a type at design-time, and the use is context-sensitive. When using import type to import a class, you can’t do things like extend from it.

ts
import type { Component } from "react";
interface ButtonProps {
// ...
}
class Button extends Component<ButtonProps> {
// ~~~~~~~~~
// error! 'Component' only refers to a type, but is being used as a value here.
// ...
}

If you’ve used Flow before, the syntax is fairly similar. One difference is that we’ve added a few restrictions to avoid code that might appear ambiguous.

ts
// Is only 'Foo' a type? Or every declaration in the import?
// We just give an error because it's not clear.
import type Foo, { Bar, Baz } from "some-module";
// ~~~~~~~~~~~~~~~~~~~~~~
// error! A type-only import can specify a default import or named bindings, but not both.

In conjunction with import type, TypeScript 3.8 also adds a new compiler flag to control what happens with imports that won’t be utilized at runtime: importsNotUsedAsValues. This flag takes 3 different values:

  • remove: this is today’s behavior of dropping these imports. It’s going to continue to be the default, and is a non-breaking change.
  • preserve: this preserves all imports whose values are never used. This can cause imports/side-effects to be preserved.
  • error: this preserves all imports (the same as the preserve option), but will error when a value import is only used as a type. This might be useful if you want to ensure no values are being accidentally imported, but still make side-effect imports explicit.

For more information about the feature, you can take a look at the pull request, and relevant changes around broadening where imports from an import type declaration can be used.

ECMAScript Private Fields

TypeScript 3.8 brings support for ECMAScript’s private fields, part of the stage-3 class fields proposal.

ts
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let jeremy = new Person("Jeremy Bearimy");
jeremy.#name;
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

Unlike regular properties (even ones declared with the private modifier), private fields have a few rules to keep in mind. Some of them are:

  • Private fields start with a # character. Sometimes we call these private names.
  • Every private field name is uniquely scoped to its containing class.
  • TypeScript accessibility modifiers like public or private can’t be used on private fields.
  • Private fields can’t be accessed or even detected outside of the containing class - even by JS users! Sometimes we call this hard privacy.

Apart from “hard” privacy, another benefit of private fields is that uniqueness we just mentioned. For example, regular property declarations are prone to being overwritten in subclasses.

ts
class C {
foo = 10;
cHelper() {
return this.foo;
}
}
class D extends C {
foo = 20;
dHelper() {
return this.foo;
}
}
let instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'

With private fields, you’ll never have to worry about this, since each field name is unique to the containing class.

ts
class C {
#foo = 10;
cHelper() {
return this.#foo;
}
}
class D extends C {
#foo = 20;
dHelper() {
return this.#foo;
}
}
let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'

Another thing worth noting is that accessing a private field on any other type will result in a TypeError!

ts
class Square {
#sideLength: number;
constructor(sideLength: number) {
this.#sideLength = sideLength;
}
equals(other: any) {
return this.#sideLength === other.#sideLength;
}
}
const a = new Square(100);
const b = { sideLength: 100 };
// Boom!
// TypeError: attempted to get private field on non-instance
// This fails because 'b' is not an instance of 'Square'.
console.log(a.equals(b));

Finally, for any plain .js file users, private fields always have to be declared before they’re assigned to.

js
class C {
// No declaration for '#foo'
// :(
constructor(foo: number) {
// SyntaxError!
// '#foo' needs to be declared before writing to it.
this.#foo = foo;
}
}

JavaScript has always allowed users to access undeclared properties, whereas TypeScript has always required declarations for class properties. With private fields, declarations are always needed regardless of whether we’re working in .js or .ts files.

js
class C {
/** @type {number} */
#foo;
constructor(foo: number) {
// This works.
this.#foo = foo;
}
}

For more information about the implementation, you can check out the original pull request

Which should I use?

We’ve already received many questions on which type of privates you should use as a TypeScript user: most commonly, “should I use the private keyword, or ECMAScript’s hash/pound (#) private fields?” It depends!

When it comes to properties, TypeScript’s private modifiers are fully erased - that means that at runtime, it acts entirely like a normal property and there’s no way to tell that it was declared with a private modifier. When using the private keyword, privacy is only enforced at compile-time/design-time, and for JavaScript consumers it’s entirely intent-based.

ts
class C {
private foo = 10;
}
// This is an error at compile time,
// but when TypeScript outputs .js files,
// it'll run fine and print '10'.
console.log(new C().foo); // prints '10'
// ~~~
// error! Property 'foo' is private and only accessible within class 'C'.
// TypeScript allows this at compile-time
// as a "work-around" to avoid the error.
console.log(new C()["foo"]); // prints '10'

The upside is that this sort of “soft privacy” can help your consumers temporarily work around not having access to some API, and also works in any runtime.

On the other hand, ECMAScript’s # privates are completely inaccessible outside of the class.

ts
class C {
#foo = 10;
}
console.log(new C().#foo); // SyntaxError
// ~~~~
// TypeScript reports an error *and*
// this won't work at runtime!
console.log(new C()["#foo"]); // prints undefined
// ~~~~~~~~~~~~~~~
// TypeScript reports an error under 'noImplicitAny',
// and this prints 'undefined'.

This hard privacy is really useful for strictly ensuring that nobody can take use of any of your internals. If you’re a library author, removing or renaming a private field should never cause a breaking change.

As we mentioned, another benefit is that subclassing can be easier with ECMAScript’s # privates because they really are private. When using ECMAScript # private fields, no subclass ever has to worry about collisions in field naming. When it comes to TypeScript’s private property declarations, users still have to be careful not to trample over properties declared in superclasses.

One more thing to think about is where you intend for your code to run. TypeScript currently can’t support this feature unless targeting ECMAScript 2015 (ES6) targets or higher. This is because our downleveled implementation uses WeakMaps to enforce privacy, and WeakMaps can’t be polyfilled in a way that doesn’t cause memory leaks. In contrast, TypeScript’s private-declared properties work with all targets - even ECMAScript 3!

A final consideration might be speed: private properties are no different from any other property, so accessing them is as fast as any other property access no matter which runtime you target. In contrast, because # private fields are downleveled using WeakMaps, they may be slower to use. While some runtimes might optimize their actual implementations of # private fields, and even have speedy WeakMap implementations, that might not be the case in all runtimes.

export * as ns Syntax

It’s often common to have a single entry-point that exposes all the members of another module as a single member.

ts
import * as utilities from "./utilities.js";
export { utilities };

This is so common that ECMAScript 2020 recently added a new syntax to support this pattern!

ts
export * as utilities from "./utilities.js";

This is a nice quality-of-life improvement to JavaScript, and TypeScript 3.8 implements this syntax. When your module target is earlier than es2020, TypeScript will output something along the lines of the first code snippet.

Top-Level await

TypeScript 3.8 provides support for a handy upcoming ECMAScript feature called “top-level await“.

JavaScript users often introduce an async function in order to use await, and then immediately called the function after defining it.

js
async function main() {
const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);
}
main().catch((e) => console.error(e));

This is because previously in JavaScript (along with most other languages with a similar feature), await was only allowed within the body of an async function. However, with top-level await, we can use await at the top level of a module.

ts
const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);
// Make sure we're a module
export {};

Note there’s a subtlety: top-level await only works at the top level of a module, and files are only considered modules when TypeScript finds an import or an export. In some basic cases, you might need to write out export {} as some boilerplate to make sure of this.

Top level await may not work in all environments where you might expect at this point. Currently, you can only use top level await when the target compiler option is es2017 or above, and module is esnext or system. Support within several environments and bundlers may be limited or may require enabling experimental support.

For more information on our implementation, you can check out the original pull request.

es2020 for target and module

TypeScript 3.8 supports es2020 as an option for module and target. This will preserve newer ECMAScript 2020 features like optional chaining, nullish coalescing, export * as ns, and dynamic import(...) syntax. It also means bigint literals now have a stable target below esnext.

JSDoc Property Modifiers

TypeScript 3.8 supports JavaScript files by turning on the allowJs flag, and also supports type-checking those JavaScript files via the checkJs option or by adding a // @ts-check comment to the top of your .js files.

Because JavaScript files don’t have dedicated syntax for type-checking, TypeScript leverages JSDoc. TypeScript 3.8 understands a few new JSDoc tags for properties.

First are the accessibility modifiers: @public, @private, and @protected. These tags work exactly like public, private, and protected respectively work in TypeScript.

js
// @ts-check
class Foo {
constructor() {
/** @private */
this.stuff = 100;
}
printStuff() {
console.log(this.stuff);
}
}
new Foo().stuff;
// ~~~~~
// error! Property 'stuff' is private and only accessible within class 'Foo'.
  • @public is always implied and can be left off, but means that a property can be reached from anywhere.
  • @private means that a property can only be used within the containing class.
  • @protected means that a property can only be used within the containing class, and all derived subclasses, but not on dissimilar instances of the containing class.

Next, we’ve also added the @readonly modifier to ensure that a property is only ever written to during initialization.

js
// @ts-check
class Foo {
constructor() {
/** @readonly */
this.stuff = 100;
}
writeToStuff() {
this.stuff = 200;
// ~~~~~
// Cannot assign to 'stuff' because it is a read-only property.
}
}
new Foo().stuff++;
// ~~~~~
// Cannot assign to 'stuff' because it is a read-only property.

Better Directory Watching on Linux and watchOptions

TypeScript 3.8 ships a new strategy for watching directories, which is crucial for efficiently picking up changes to node_modules.

For some context, on operating systems like Linux, TypeScript installs directory watchers (as opposed to file watchers) on node_modules and many of its subdirectories to detect changes in dependencies. This is because the number of available file watchers is often eclipsed by the of files in node_modules, whereas there are way fewer directories to track.

Older versions of TypeScript would immediately install directory watchers on folders, and at startup that would be fine; however, during an npm install, a lot of activity will take place within node_modules and that can overwhelm TypeScript, often slowing editor sessions to a crawl. To prevent this, TypeScript 3.8 waits slightly before installing directory watchers to give these highly volatile directories some time to stabilize.

Because every project might work better under different strategies, and this new approach might not work well for your workflows, TypeScript 3.8 introduces a new watchOptions field in tsconfig.json and jsconfig.json which allows users to tell the compiler/language service which watching strategies should be used to keep track of files and directories.

{
// Some typical compiler options
"": "es2020",
"": "node"
// ...
},
// NEW: Options for file/directory watching
"watchOptions": {
// Use native file system events for files and directories
"": "useFsEvents",
"": "useFsEvents",
// Poll files for updates more frequently
// when they're updated a lot.
"": "dynamicPriority"
}
}

watchOptions contains 4 new options that can be configured to handle how TypeScript keeps track of changes.

For more information on these changes, head over to GitHub to see the pull request to read more.

“Fast and Loose” Incremental Checking

TypeScript 3.8 introduces a new compiler option called assumeChangesOnlyAffectDirectDependencies. When this option is enabled, TypeScript will avoid rechecking/rebuilding all truly possibly-affected files, and only recheck/rebuild files that have changed as well as files that directly import them.

In a codebase like Visual Studio Code, this reduced rebuild times for changes in certain files from about 14 seconds to about 1 second. While we don’t necessarily recommend this option for all codebases, you might be interested if you have an extremely large codebase and are willing to defer full project errors until later (e.g. a dedicated build via a tsconfig.fullbuild.json or in CI).

For more details, you can see the original pull request.

TypeScript 3.7

Optional Chaining

Playground

Optional chaining is issue #16 on our issue tracker. For context, there have been over 23,000 issues on the TypeScript issue tracker since then.

At its core, optional chaining lets us write code where TypeScript can immediately stop running some expressions if we run into a null or undefined. The star of the show in optional chaining is the new ?. operator for optional property accesses. When we write code like

ts
let x = foo?.bar.baz();

this is a way of saying that when foo is defined, foo.bar.baz() will be computed; but when foo is null or undefined, stop what we’re doing and just return undefined.”

More plainly, that code snippet is the same as writing the following.

ts
let x = foo === null || foo === undefined ? undefined : foo.bar.baz();

Note that if bar is null or undefined, our code will still hit an error accessing baz. Likewise, if baz is null or undefined, we’ll hit an error at the call site. ?. only checks for whether the value on the left of it is null or undefined - not any of the subsequent properties.

You might find yourself using ?. to replace a lot of code that performs repetitive nullish checks using the && operator.

ts
// Before
if (foo && foo.bar && foo.bar.baz) {
// ...
}
// After-ish
if (foo?.bar?.baz) {
// ...
}

Keep in mind that ?. acts differently than those && operations since && will act specially on “falsy” values (e.g. the empty string, 0, NaN, and, well, false), but this is an intentional feature of the construct. It doesn’t short-circuit on valid data like 0 or empty strings.

Optional chaining also includes two other operations. First there’s the optional element access which acts similarly to optional property accesses, but allows us to access non-identifier properties (e.g. arbitrary strings, numbers, and symbols):

ts
/**
* Get the first element of the array if we have an array.
* Otherwise return undefined.
*/
function tryGetFirstElement<T>(arr?: T[]) {
return arr?.[0];
// equivalent to
// return (arr === null || arr === undefined) ?
// undefined :
// arr[0];
}

There’s also optional call, which allows us to conditionally call expressions if they’re not null or undefined.

ts
async function makeRequest(url: string, log?: (msg: string) => void) {
log?.(`Request started at ${new Date().toISOString()}`);
// roughly equivalent to
// if (log != null) {
// log(`Request started at ${new Date().toISOString()}`);
// }
const result = (await fetch(url)).json();
log?.(`Request finished at at ${new Date().toISOString()}`);
return result;
}

The “short-circuiting” behavior that optional chains have is limited property accesses, calls, element accesses - it doesn’t expand any further out from these expressions. In other words,

ts
let result = foo?.bar / someComputation();

doesn’t stop the division or someComputation() call from occurring. It’s equivalent to

ts
let temp = foo === null || foo === undefined ? undefined : foo.bar;
let result = temp / someComputation();

That might result in dividing undefined, which is why in strictNullChecks, the following is an error.

ts
function barPercentage(foo?: { bar: number }) {
return foo?.bar / 100;
// ~~~~~~~~
// Error: Object is possibly undefined.
}

More more details, you can read up on the proposal and view the original pull request.

Nullish Coalescing

Playground

The nullish coalescing operator is another upcoming ECMAScript feature that goes hand-in-hand with optional chaining, and which our team has been involved with championing in TC39.

You can think of this feature - the ?? operator - as a way to “fall back” to a default value when dealing with null or undefined. When we write code like

ts
let x = foo ?? bar();

this is a new way to say that the value foo will be used when it’s “present”; but when it’s null or undefined, calculate bar() in its place.

Again, the above code is equivalent to the following.

ts
let x = foo !== null && foo !== undefined ? foo : bar();

The ?? operator can replace uses of || when trying to use a default value. For example, the following code snippet tries to fetch the volume that was last saved in localStorage (if it ever was); however, it has a bug because it uses ||.

ts
function initializeAudio() {
let volume = localStorage.volume || 0.5;
// ...
}

When localStorage.volume is set to 0, the page will set the volume to 0.5 which is unintended. ?? avoids some unintended behavior from 0, NaN and "" being treated as falsy values.

We owe a large thanks to community members Wenlu Wang and Titian Cernicova Dragomir for implementing this feature! For more details, check out their pull request and the nullish coalescing proposal repository.

Assertion Functions

Playground

There’s a specific set of functions that throw an error if something unexpected happened. They’re called “assertion” functions. As an example, Node.js has a dedicated function for this called assert.

js
assert(someValue === 42);

In this example if someValue isn’t equal to 42, then assert will throw an AssertionError.

Assertions in JavaScript are often used to guard against improper types being passed in. For example,

js
function multiply(x, y) {
assert(typeof x === "number");
assert(typeof y === "number");
return x * y;
}

Unfortunately in TypeScript these checks could never be properly encoded. For loosely-typed code this meant TypeScript was checking less, and for slightly conservative code it often forced users to use type assertions.

ts
function yell(str) {
assert(typeof str === "string");
return str.toUppercase();
// Oops! We misspelled 'toUpperCase'.
// Would be great if TypeScript still caught this!
}

The alternative was to instead rewrite the code so that the language could analyze it, but this isn’t convenient.

ts
function yell(str) {
if (typeof str !== "string") {
throw new TypeError("str should have been a string.");
}
// Error caught!
return str.toUppercase();
}

Ultimately the goal of TypeScript is to type existing JavaScript constructs in the least disruptive way. For that reason, TypeScript 3.7 introduces a new concept called “assertion signatures” which model these assertion functions.

The first type of assertion signature models the way that Node’s assert function works. It ensures that whatever condition is being checked must be true for the remainder of the containing scope.

ts
function assert(condition: any, msg?: string): asserts condition {
if (!condition) {
throw new AssertionError(msg);
}
}

asserts condition says that whatever gets passed into the condition parameter must be true if the assert returns (because otherwise it would throw an error). That means that for the rest of the scope, that condition must be truthy. As an example, using this assertion function means we do catch our original yell example.

ts
function yell(str) {
assert(typeof str === "string");
return str.toUppercase();
// ~~~~~~~~~~~
// error: Property 'toUppercase' does not exist on type 'string'.
// Did you mean 'toUpperCase'?
}
function assert(condition: any, msg?: string): asserts condition {
if (!condition) {
throw new AssertionError(msg);
}
}

The other type of assertion signature doesn’t check for a condition, but instead tells TypeScript that a specific variable or property has a different type.

ts
function assertIsString(val: any): asserts val is string {
if (typeof val !== "string") {
throw new AssertionError("Not a string!");
}
}

Here asserts val is string ensures that after any call to assertIsString, any variable passed in will be known to be a string.

ts
function yell(str: any) {
assertIsString(str);
// Now TypeScript knows that 'str' is a 'string'.
return str.toUppercase();
// ~~~~~~~~~~~
// error: Property 'toUppercase' does not exist on type 'string'.
// Did you mean 'toUpperCase'?
}

These assertion signatures are very similar to writing type predicate signatures:

ts
function isString(val: any): val is string {
return typeof val === "string";
}
function yell(str: any) {
if (isString(str)) {
return str.toUppercase();
}
throw "Oops!";
}

And just like type predicate signatures, these assertion signatures are incredibly expressive. We can express some fairly sophisticated ideas with these.

ts
function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
if (val === undefined || val === null) {
throw new AssertionError(
`Expected 'val' to be defined, but received ${val}`
);
}
}

To read up more about assertion signatures, check out the original pull request.

Better Support for never-Returning Functions

As part of the work for assertion signatures, TypeScript needed to encode more about where and which functions were being called. This gave us the opportunity to expand support for another class of functions: functions that return never.

The intent of any function that returns never is that it never returns. It indicates that an exception was thrown, a halting error condition occurred, or that the program exited. For example, process.exit(...) in @types/node is specified to return never.

In order to ensure that a function never potentially returned undefined or effectively returned from all code paths, TypeScript needed some syntactic signal - either a return or throw at the end of a function. So users found themselves return-ing their failure functions.

ts
function dispatch(x: string | number): SomeType {
if (typeof x === "string") {
return doThingWithString(x);
} else if (typeof x === "number") {
return doThingWithNumber(x);
}
return process.exit(1);
}

Now when these never-returning functions are called, TypeScript recognizes that they affect the control flow graph and accounts for them.

ts
function dispatch(x: string | number): SomeType {
if (typeof x === "string") {
return doThingWithString(x);
} else if (typeof x === "number") {
return doThingWithNumber(x);
}
process.exit(1);
}

As with assertion functions, you can read up more at the same pull request.

(More) Recursive Type Aliases

Playground

Type aliases have always had a limitation in how they could be “recursively” referenced. The reason is that any use of a type alias needs to be able to substitute itself with whatever it aliases. In some cases, that’s not possible, so the compiler rejects certain recursive aliases like the following:

ts
type Foo = Foo;

This is a reasonable restriction because any use of Foo would need to be replaced with Foo which would need to be replaced with Foo which would need to be replaced with Foo which… well, hopefully you get the idea! In the end, there isn’t a type that makes sense in place of Foo.

This is fairly consistent with how other languages treat type aliases, but it does give rise to some slightly surprising scenarios for how users leverage the feature. For example, in TypeScript 3.6 and prior, the following causes an error.

ts
type ValueOrArray<T> = T | Array<ValueOrArray<T>>;
// ~~~~~~~~~~~~
// error: Type alias 'ValueOrArray' circularly references itself.

This is strange because there is technically nothing wrong with any use users could always write what was effectively the same code by introducing an interface.

ts
type ValueOrArray<T> = T | ArrayOfValueOrArray<T>;
interface ArrayOfValueOrArray<T> extends Array<ValueOrArray<T>> {}

Because interfaces (and other object types) introduce a level of indirection and their full structure doesn’t need to be eagerly built out, TypeScript has no problem working with this structure.

But workaround of introducing the interface wasn’t intuitive for users. And in principle there really wasn’t anything wrong with the original version of ValueOrArray that used Array directly. If the compiler was a little bit “lazier” and only calculated the type arguments to Array when necessary, then TypeScript could express these correctly.

That’s exactly what TypeScript 3.7 introduces. At the “top level” of a type alias, TypeScript will defer resolving type arguments to permit these patterns.

This means that code like the following that was trying to represent JSON…

ts
type Json = string | number | boolean | null | JsonObject | JsonArray;
interface JsonObject {
[property: string]: Json;
}
interface JsonArray extends Array<Json> {}

can finally be rewritten without helper interfaces.

ts
type Json =
| string
| number
| boolean
| null
| { [property: string]: Json }
| Json[];

This new relaxation also lets us recursively reference type aliases in tuples as well. The following code which used to error is now valid TypeScript code.

ts
type VirtualNode = string | [string, { [key: string]: any }, ...VirtualNode[]];
const myNode: VirtualNode = [
"div",
{ id: "parent" },
["div", { id: "first-child" }, "I'm the first child"],
["div", { id: "second-child" }, "I'm the second child"],
];

For more information, you can read up on the original pull request.

--declaration and --allowJs

The --declaration flag in TypeScript allows us to generate .d.ts files (declaration files) from TypeScript source files (i.e. .ts and .tsx files). These .d.ts files are important for a couple of reasons.

First of all, they’re important because they allow TypeScript to type-check against other projects without re-checking the original source code. They’re also important because they allow TypeScript to interoperate with existing JavaScript libraries that weren’t built with TypeScript in mind. Finally, a benefit that is often underappreciated: both TypeScript and JavaScript users can benefit from these files when using editors powered by TypeScript to get things like better auto-completion.

Unfortunately, --declaration didn’t work with the --allowJs flag which allows mixing TypeScript and JavaScript input files. This was a frustrating limitation because it meant users couldn’t use the --declaration flag when migrating codebases, even if they were JSDoc-annotated. TypeScript 3.7 changes that, and allows the two options to be used together!

The most impactful outcome of this feature might a bit subtle: with TypeScript 3.7, users can write libraries in JSDoc annotated JavaScript and support TypeScript users.

The way that this works is that when using allowJs, TypeScript has some best-effort analyses to understand common JavaScript patterns; however, the way that some patterns are expressed in JavaScript don’t necessarily look like their equivalents in TypeScript. When declaration emit is turned on, TypeScript figures out the best way to transform JSDoc comments and CommonJS exports into valid type declarations and the like in the output .d.ts files.

As an example, the following code snippet

js
const assert = require("assert");
module.exports.blurImage = blurImage;
/**
* Produces a blurred image from an input buffer.
*
* @param input {Uint8Array}
* @param width {number}
* @param height {number}
*/
function blurImage(input, width, height) {
const numPixels = width * height * 4;
assert(input.length === numPixels);
const result = new Uint8Array(numPixels);
// TODO
return result;
}

Will produce a .d.ts file like

ts
/**
* Produces a blurred image from an input buffer.
*
* @param input {Uint8Array}
* @param width {number}
* @param height {number}
*/
export function blurImage(
input: Uint8Array,
width: number,
height: number
): Uint8Array;

This can go beyond basic functions with @param tags too, where the following example:

js
/**
* @callback Job
* @returns {void}
*/
/** Queues work */
export class Worker {
constructor(maxDepth = 10) {
this.started = false;
this.depthLimit = maxDepth;
/**
* NOTE: queued jobs may add more items to queue
* @type {Job[]}
*/
this.queue = [];
}
/**
* Adds a work item to the queue
* @param {Job} work
*/
push(work) {
if (this.queue.length + 1 > this.depthLimit) throw new Error("Queue full!");
this.queue.push(work);
}
/**
* Starts the queue if it has not yet started
*/
start() {
if (this.started) return false;
this.started = true;
while (this.queue.length) {
/** @type {Job} */ (this.queue.shift())();
}
return true;
}
}

will be transformed into the following .d.ts file:

ts
/**
* @callback Job
* @returns {void}
*/
/** Queues work */
export class Worker {
constructor(maxDepth?: number);
started: boolean;
depthLimit: number;
/**
* NOTE: queued jobs may add more items to queue
* @type {Job[]}
*/
queue: Job[];
/**
* Adds a work item to the queue
* @param {Job} work
*/
push(work: Job): void;
/**
* Starts the queue if it has not yet started
*/
start(): boolean;
}
export type Job = () => void;

Note that when using these flags together, TypeScript doesn’t necessarily have to downlevel .js files. If you simply want TypeScript to create .d.ts files, you can use the --emitDeclarationOnly compiler option.

For more details, you can check out the original pull request.

The useDefineForClassFields Flag and The declare Property Modifier

Back when TypeScript implemented public class fields, we assumed to the best of our abilities that the following code

ts
class C {
foo = 100;
bar: string;
}

would be equivalent to a similar assignment within a constructor body.

ts
class C {
constructor() {
this.foo = 100;
}
}

Unfortunately, while this seemed to be the direction that the proposal moved towards in its earlier days, there is an extremely strong chance that public class fields will be standardized differently. Instead, the original code sample might need to de-sugar to something closer to the following:

ts
class C {
constructor() {
Object.defineProperty(this, "foo", {
enumerable: true,
configurable: true,
writable: true,
value: 100,
});
Object.defineProperty(this, "bar", {
enumerable: true,
configurable: true,
writable: true,
value: void 0,
});
}
}

While TypeScript 3.7 isn’t changing any existing emit by default, we’ve been rolling out changes incrementally to help users mitigate potential future breakage. We’ve provided a new flag called useDefineForClassFields to enable this emit mode with some new checking logic.

The two biggest changes are the following:

  • Declarations are initialized with Object.defineProperty.
  • Declarations are always initialized to undefined, even if they have no initializer.

This can cause quite a bit of fallout for existing code that use inheritance. First of all, set accessors from base classes won’t get triggered - they’ll be completely overwritten.

ts
class Base {
set data(value: string) {
console.log("data changed to " + value);
}
}
class Derived extends Base {
// No longer triggers a 'console.log'
// when using 'useDefineForClassFields'.
data = 10;
}

Secondly, using class fields to specialize properties from base classes also won’t work.

ts
interface Animal {
animalStuff: any;
}
interface Dog extends Animal {
dogStuff: any;
}
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
// Initializes 'resident' to 'undefined'
// after the call to 'super()' when
// using 'useDefineForClassFields'!
resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}

What these two boil down to is that mixing properties with accessors is going to cause issues, and so will re-declaring properties with no initializers.

To detect the issue around accessors, TypeScript 3.7 will now emit get/set accessors in .d.ts files so that in TypeScript can check for overridden accessors.

Code that’s impacted by the class fields change can get around the issue by converting field initializers to assignments in constructor bodies.

ts
class Base {
set data(value: string) {
console.log("data changed to " + value);
}
}
class Derived extends Base {
constructor() {
data = 10;
}
}

To help mitigate the second issue, you can either add an explicit initializer or add a declare modifier to indicate that a property should have no emit.

ts
interface Animal {
animalStuff: any;
}
interface Dog extends Animal {
dogStuff: any;
}
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
class DogHouse extends AnimalHouse {
declare resident: Dog;
// ^^^^^^^
// 'resident' now has a 'declare' modifier,
// and won't produce any output code.
constructor(dog: Dog) {
super(dog);
}
}

Currently useDefineForClassFields is only available when targeting ES5 and upwards, since Object.defineProperty doesn’t exist in ES3. To achieve similar checking for issues, you can create a separate project that targets ES5 and uses --noEmit to avoid a full build.

For more information, you can take a look at the original pull request for these changes.

We strongly encourage users to try the useDefineForClassFields flag and report back on our issue tracker or in the comments below. This includes feedback on difficulty of adopting the flag so we can understand how we can make migration easier.

Build-Free Editing with Project References

TypeScript’s project references provide us with an easy way to break codebases up to give us faster compiles. Unfortunately, editing a project whose dependencies hadn’t been built (or whose output was out of date) meant that the editing experience wouldn’t work well.

In TypeScript 3.7, when opening a project with dependencies, TypeScript will automatically use the source .ts/.tsx files instead. This means projects using project references will now see an improved editing experience where semantic operations are up-to-date and “just work”. You can disable this behavior with the compiler option disableSourceOfProjectReferenceRedirect which may be appropriate when working in very large projects where this change may impact editing performance.

You can read up more about this change by reading up on its pull request.

Uncalled Function Checks

A common and dangerous error is to forget to invoke a function, especially if the function has zero arguments or is named in a way that implies it might be a property rather than a function.

ts
interface User {
isAdministrator(): boolean;
notify(): void;
doNotDisturb?(): boolean;
}
// later...
// Broken code, do not use!
function doAdminThing(user: User) {
// oops!
if (user.isAdministrator) {
sudo();
editTheConfiguration();
} else {
throw new AccessDeniedError("User is not an admin");
}
}

Here, we forgot to call isAdministrator, and the code incorrectly allows non-adminstrator users to edit the configuration!

In TypeScript 3.7, this is identified as a likely error:

ts
function doAdminThing(user: User) {
if (user.isAdministrator) {
// ~~~~~~~~~~~~~~~~~~~~
// error! This condition will always return true since the function is always defined.
// Did you mean to call it instead?

This check is a breaking change, but for that reason the checks are very conservative. This error is only issued in if conditions, and it is not issued on optional properties, if strictNullChecks is off, or if the function is later called within the body of the if:

ts
interface User {
isAdministrator(): boolean;
notify(): void;
doNotDisturb?(): boolean;
}
function issueNotification(user: User) {
if (user.doNotDisturb) {
// OK, property is optional
}
if (user.notify) {
// OK, called the function
user.notify();
}
}

If you intended to test the function without calling it, you can correct the definition of it to include undefined/null, or use !! to write something like if (!!user.isAdministrator) to indicate that the coercion is intentional.

We owe a big thanks to GitHub user @jwbay who took the initiative to create a proof-of-concept and iterated to provide us with with the current version.

// @ts-nocheck in TypeScript Files

TypeScript 3.7 allows us to add // @ts-nocheck comments to the top of TypeScript files to disable semantic checks. Historically this comment was only respected in JavaScript source files in the presence of checkJs, but we’ve expanded support to TypeScript files to make migrations easier for all users.

Semicolon Formatter Option

TypeScript’s built-in formatter now supports semicolon insertion and removal at locations where a trailing semicolon is optional due to JavaScript’s automatic semicolon insertion (ASI) rules. The setting is available now in Visual Studio Code Insiders, and will be available in Visual Studio 16.4 Preview 2 in the Tools Options menu.

New semicolon formatter option in VS Code

Choosing a value of “insert” or “remove” also affects the format of auto-imports, extracted types, and other generated code provided by TypeScript services. Leaving the setting on its default value of “ignore” makes generated code match the semicolon preference detected in the current file.

3.7 Breaking Changes

DOM Changes

Types in lib.dom.d.ts have been updated. These changes are largely correctness changes related to nullability, but impact will ultimately depend on your codebase.

Class Field Mitigations

As mentioned above, TypeScript 3.7 emits get/set accessors in .d.ts files which can cause breaking changes for consumers on older versions of TypeScript like 3.5 and prior. TypeScript 3.6 users will not be impacted, since that version was future-proofed for this feature.

While not a breakage per se, opting in to the useDefineForClassFields flag can cause breakage when:

  • overriding an accessor in a derived class with a property declaration
  • re-declaring a property declaration with no initializer

To understand the full impact, read the section above on the useDefineForClassFields flag.

Function Truthy Checks

As mentioned above, TypeScript now errors when functions appear to be uncalled within if statement conditions. An error is issued when a function type is checked in if conditions unless any of the following apply:

  • the checked value comes from an optional property
  • strictNullChecks is disabled
  • the function is later called within the body of the if

Local and Imported Type Declarations Now Conflict

Due to a bug, the following construct was previously allowed in TypeScript:

ts
// ./someOtherModule.ts
interface SomeType {
y: string;
}
// ./myModule.ts
import { SomeType } from "./someOtherModule";
export interface SomeType {
x: number;
}
function fn(arg: SomeType) {
console.log(arg.x); // Error! 'x' doesn't exist on 'SomeType'
}

Here, SomeType appears to originate in both the import declaration and the local interface declaration. Perhaps surprisingly, inside the module, SomeType refers exclusively to the imported definition, and the local declaration SomeType is only usable when imported from another file. This is very confusing and our review of the very small number of cases of code like this in the wild showed that developers usually thought something different was happening.

In TypeScript 3.7, this is now correctly identified as a duplicate identifier error. The correct fix depends on the original intent of the author and should be addressed on a case-by-case basis. Usually, the naming conflict is unintentional and the best fix is to rename the imported type. If the intent was to augment the imported type, a proper module augmentation should be written instead.

3.7 API Changes

To enable the recursive type alias patterns described above, the typeArguments property has been removed from the TypeReference interface. Users should instead use the getTypeArguments function on TypeChecker instances.

TypeScript 3.6

Stricter Generators

TypeScript 3.6 introduces stricter checking for iterators and generator functions. In earlier versions, users of generators had no way to differentiate whether a value was yielded or returned from a generator.

ts
function* foo() {
if (Math.random() < 0.5) yield 100;
return "Finished!";
}
let iter = foo();
let curr = iter.next();
if (curr.done) {
// TypeScript 3.5 and prior thought this was a 'string | number'.
// It should know it's 'string' since 'done' was 'true'!
curr.value;
}

Additionally, generators just assumed the type of yield was always any.

ts
function* bar() {
let x: { hello(): void } = yield;
x.hello();
}
let iter = bar();
iter.next();
iter.next(123); // oops! runtime error!

In TypeScript 3.6, the checker now knows that the correct type for curr.value should be string in our first example, and will correctly error on our call to next() in our last example. This is thanks to some changes in the Iterator and IteratorResult type declarations to include a few new type parameters, and to a new type that TypeScript uses to represent generators called the Generator type.

The Iterator type now allows users to specify the yielded type, the returned type, and the type that next can accept.

ts
interface Iterator<T, TReturn = any, TNext = undefined> {
// Takes either 0 or 1 arguments - doesn't accept 'undefined'
next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
return?(value?: TReturn): IteratorResult<T, TReturn>;
throw?(e?: any): IteratorResult<T, TReturn>;
}

Building on that work, the new Generator type is an Iterator that always has both the return and throw methods present, and is also iterable.

ts
interface Generator<T = unknown, TReturn = any, TNext = unknown>
extends Iterator<T, TReturn, TNext> {
next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
return(value: TReturn): IteratorResult<T, TReturn>;
throw(e: any): IteratorResult<T, TReturn>;
[Symbol.iterator](): Generator<T, TReturn, TNext>;
}

To allow differentiation between returned values and yielded values, TypeScript 3.6 converts the IteratorResult type to a discriminated union type:

ts
type IteratorResult<T, TReturn = any> =
| IteratorYieldResult<T>
| IteratorReturnResult<TReturn>;
interface IteratorYieldResult<TYield> {
done?: false;
value: TYield;
}
interface IteratorReturnResult<TReturn> {
done: true;
value: TReturn;
}

In short, what this means is that you’ll be able to appropriately narrow down values from iterators when dealing with them directly.

To correctly represent the types that can be passed in to a generator from calls to next(), TypeScript 3.6 also infers certain uses of yield within the body of a generator function.

ts
function* foo() {
let x: string = yield;
console.log(x.toUpperCase());
}
let x = foo();
x.next(); // first call to 'next' is always ignored
x.next(42); // error! 'number' is not assignable to 'string'

If you’d prefer to be explicit, you can also enforce the type of values that can be returned, yielded, and evaluated from yield expressions using an explicit return type. Below, next() can only be called with booleans, and depending on the value of done, value is either a string or a number.

ts
/**
* - yields numbers
* - returns strings
* - can be passed in booleans
*/
function* counter(): Generator<number, string, boolean> {
let i = 0;
while (true) {
if (yield i++) {
break;
}
}
return "done!";
}
var iter = counter();
var curr = iter.next();
while (!curr.done) {
console.log(curr.value);
curr = iter.next(curr.value === 5);
}
console.log(curr.value.toUpperCase());
// prints:
//
// 0
// 1
// 2
// 3
// 4
// 5
// DONE!

For more details on the change, see the pull request here.

More Accurate Array Spread

In pre-ES2015 targets, the most faithful emit for constructs like for/of loops and array spreads can be a bit heavy. For this reason, TypeScript uses a simpler emit by default that only supports array types, and supports iterating on other types using the --downlevelIteration flag. The looser default without --downlevelIteration works fairly well; however, there were some common cases where the transformation of array spreads had observable differences. For example, the following array containing a spread

ts
[...Array(5)];

can be rewritten as the following array literal

js
[undefined, undefined, undefined, undefined, undefined];

However, TypeScript would instead transform the original code into this code:

ts
Array(5).slice();

which is slightly different. Array(5) produces an array with a length of 5, but with no defined property slots.

TypeScript 3.6 introduces a new __spreadArrays helper to accurately model what happens in ECMAScript 2015 in older targets outside of --downlevelIteration. __spreadArrays is also available in tslib.

For more information, see the relevant pull request.

Improved UX Around Promises

TypeScript 3.6 introduces some improvements for when Promises are mis-handled.

For example, it’s often very common to forget to .then() or await the contents of a Promise before passing it to another function. TypeScript’s error messages are now specialized, and inform the user that perhaps they should consider using the await keyword.

ts
interface User {
name: string;
age: number;
location: string;
}
declare function getUserData(): Promise<User>;
declare function displayUser(user: User): void;
async function f() {
displayUser(getUserData());
// ~~~~~~~~~~~~~
// Argument of type 'Promise<User>' is not assignable to parameter of type 'User'.
// ...
// Did you forget to use 'await'?
}

It’s also common to try to access a method before await-ing or .then()-ing a Promise. This is another example, among many others, where we’re able to do better.

ts
async function getCuteAnimals() {
fetch("https://reddit.com/r/aww.json").json();
// ~~~~
// Property 'json' does not exist on type 'Promise<Response>'.
//
// Did you forget to use 'await'?
}

For more details, see the originating issue, as well as the pull requests that link back to it.

Better Unicode Support for Identifiers

TypeScript 3.6 contains better support for Unicode characters in identifiers when emitting to ES2015 and later targets.

ts
const 𝓱𝓮𝓵𝓵𝓸 = "world"; // previously disallowed, now allowed in '--target es2015'

import.meta Support in SystemJS

TypeScript 3.6 supports transforming import.meta to context.meta when your module target is set to system.

ts
// This module:
console.log(import.meta.url);
// gets turned into the following:
System.register([], function (exports, context) {
return {
setters: [],
execute: function () {
console.log(context.meta.url);
},
};
});

get and set Accessors Are Allowed in Ambient Contexts

In previous versions of TypeScript, the language didn’t allow get and set accessors in ambient contexts (like in declare-d classes, or in .d.ts files in general). The rationale was that accessors weren’t distinct from properties as far as writing and reading to these properties; however, because ECMAScript’s class fields proposal may have differing behavior from in existing versions of TypeScript, we realized we needed a way to communicate this different behavior to provide appropriate errors in subclasses.

As a result, users can write getters and setters in ambient contexts in TypeScript 3.6.

ts
declare class Foo {
// Allowed in 3.6+.
get x(): number;
set x(val: number);
}

In TypeScript 3.7, the compiler itself will take advantage of this feature so that generated .d.ts files will also emit get/set accessors.

Ambient Classes and Functions Can Merge

In previous versions of TypeScript, it was an error to merge classes and functions under any circumstances. Now, ambient classes and functions (classes/functions with the declare modifier, or in .d.ts files) can merge. This means that now you can write the following:

ts
export declare function Point2D(x: number, y: number): Point2D;
export declare class Point2D {
x: number;
y: number;
constructor(x: number, y: number);
}

instead of needing to use

ts
export interface Point2D {
x: number;
y: number;
}
export declare var Point2D: {
(x: number, y: number): Point2D;
new (x: number, y: number): Point2D;
};

One advantage of this is that the callable constructor pattern can be easily expressed while also allowing namespaces to merge with these declarations (since var declarations can’t merge with namespaces).

In TypeScript 3.7, the compiler will take advantage of this feature so that .d.ts files generated from .js files can appropriately capture both the callability and constructability of a class-like function.

For more details, see the original PR on GitHub.

APIs to Support --build and --incremental

TypeScript 3.0 introduced support for referencing other and building them incrementally using the --build flag. Additionally, TypeScript 3.4 introduced the --incremental flag for saving information about previous compilations to only rebuild certain files. These flags were incredibly useful for structuring projects more flexibly and speeding builds up. Unfortunately, using these flags didn’t work with 3rd party build tools like Gulp and Webpack. TypeScript 3.6 now exposes two sets of APIs to operate on project references and incremental program building.

For creating --incremental builds, users can leverage the createIncrementalProgram and createIncrementalCompilerHost APIs. Users can also re-hydrate old program instances from .tsbuildinfo files generated by this API using the newly exposed readBuilderProgram function, which is only meant to be used as for creating new programs (i.e. you can’t modify the returned instance - it’s only meant to be used for the oldProgram parameter in other create*Program functions).

For leveraging project references, a new createSolutionBuilder function has been exposed, which returns an instance of the new type SolutionBuilder.

For more details on these APIs, you can see the original pull request.

Semicolon-Aware Code Edits

Editors like Visual Studio and Visual Studio Code can automatically apply quick fixes, refactorings, and other transformations like automatically importing values from other modules. These transformations are powered by TypeScript, and older versions of TypeScript unconditionally added semicolons to the end of every statement; unfortunately, this disagreed with many users’ style guidelines, and many users were displeased with the editor inserting semicolons.

TypeScript is now smart enough to detect whether your file uses semicolons when applying these sorts of edits. If your file generally lacks semicolons, TypeScript won’t add one.

For more details, see the corresponding pull request.

Smarter Auto-Import Syntax

JavaScript has a lot of different module syntaxes or conventions: the one in the ECMAScript standard, the one Node already supports (CommonJS), AMD, System.js, and more! For the most part, TypeScript would default to auto-importing using ECMAScript module syntax, which was often inappropriate in certain TypeScript projects with different compiler settings, or in Node projects with plain JavaScript and require calls.

TypeScript 3.6 is now a bit smarter about looking at your existing imports before deciding on how to auto-import other modules. You can see more details in the original pull request here.

await Completions on Promises

New TypeScript Playground

The TypeScript playground has received a much-needed refresh with handy new functionality! The new playground is largely a fork of Artem Tyurin’s TypeScript playground which community members have been using more and more. We owe Artem a big thanks for helping out here!

The new playground now supports many new options including:

  • The target option (allowing users to switch out of es5 to es3, es2015, esnext, etc.)
  • All the strictness flags (including just strict)
  • Support for plain JavaScript files (using allowJS and optionally checkJs)

These options also persist when sharing links to playground samples, allowing users to more reliably share examples without having to tell the recipient “oh, don’t forget to turn on the noImplicitAny option!“.

In the near future, we’re going to be refreshing the playground samples, adding JSX support, and polishing automatic type acquisition, meaning that you’ll be able to see the same experience on the playground as you’d get in your personal editor.

As we improve the playground and the website, we welcome feedback and pull requests on GitHub!

TypeScript 3.5

Speed improvements

TypeScript 3.5 introduces several optimizations around type-checking and incremental builds.

Type-checking speed-ups

TypeScript 3.5 contains certain optimizations over TypeScript 3.4 for type-checking more efficiently. These improvements are significantly more pronounced in editor scenarios where type-checking drives operations like code completion lists.

--incremental improvements

TypeScript 3.5 improves on 3.4’s --incremental build mode, by saving information about how the state of the world was calculated - compiler settings, why files were looked up, where files were found, etc. In scenarios involving hundreds of projects using TypeScript’s project references in --build mode, we’ve found that the amount of time rebuilding can be reduced by as much as 68% compared to TypeScript 3.4!

For more details, you can see the pull requests to

The Omit helper type

TypeScript 3.5 introduces the new Omit helper type, which creates a new type with some properties dropped from the original.

ts
type Person = {
name: string;
age: number;
location: string;
};
type QuantumPerson = Omit<Person, "location">;
// equivalent to
type QuantumPerson = {
name: string;
age: number;
};

Here we were able to copy over all the properties of Person except for location using the Omit helper.

For more details, see the pull request on GitHub to add Omit, as well as the change to use Omit for object rest.

Improved excess property checks in union types

In TypeScript 3.4 and earlier, certain excess properties were allowed in situations where they really shouldn’t have been. For instance, TypeScript 3.4 permitted the incorrect name property in the object literal even though its types don’t match between Point and Label.

ts
type Point = {
x: number;
y: number;
};
type Label = {
name: string;
};
const thing: Point | Label = {
x: 0,
y: 0,
name: true, // uh-oh!
};

Previously, a non-disciminated union wouldn’t have any excess property checking done on its members, and as a result, the incorrectly typed name property slipped by.

In TypeScript 3.5, the type-checker at least verifies that all the provided properties belong to some union member and have the appropriate type, meaning that the sample above correctly issues an error.

Note that partial overlap is still permitted as long as the property types are valid.

ts
const pl: Point | Label = {
x: 0,
y: 0,
name: "origin", // okay
};

The --allowUmdGlobalAccess flag

In TypeScript 3.5, you can now reference UMD global declarations like

export as namespace foo;

from anywhere - even modules - using the new --allowUmdGlobalAccess flag.

This mode adds flexibility for mixing and matching the way 3rd party libraries, where globals that libraries declare can always be consumed, even from within modules.

For more details, see the pull request on GitHub.

Smarter union type checking

In TypeScript 3.4 and prior, the following example would fail:

ts
type S = { done: boolean; value: number };
type T = { done: false; value: number } | { done: true; value: number };
declare let source: S;
declare let target: T;
target = source;

That’s because S isn’t assignable to { done: false, value: number } nor { done: true, value: number }. Why? Because the done property in S isn’t specific enough - it’s boolean whereas each constituent of T has a done property that’s specifically true or false. That’s what we meant by each constituent type being checked in isolation: TypeScript doesn’t just union each property together and see if S is assignable to that. If it did, some bad code could get through like the following:

ts
interface Foo {
kind: "foo";
value: string;
}
interface Bar {
kind: "bar";
value: number;
}
function doSomething(x: Foo | Bar) {
if (x.kind === "foo") {
x.value.toLowerCase();
}
}
// uh-oh - luckily TypeScript errors here!
doSomething({
kind: "foo",
value: 123,
});

However, this was a bit overly strict for the original example. If you figure out the precise type of any possible value of S, you can actually see that it matches the types in T exactly.

In TypeScript 3.5, when assigning to types with discriminant properties like in T, the language actually will go further and decompose types like S into a union of every possible inhabitant type. In this case, since boolean is a union of true and false, S will be viewed as a union of { done: false, value: number } and { done: true, value: number }.

For more details, you can see the original pull request on GitHub.

Higher order type inference from generic constructors

In TypeScript 3.4, we improved inference for when generic functions that return functions like so:

ts
function compose<T, U, V>(f: (x: T) => U, g: (y: U) => V): (x: T) => V {
return (x) => g(f(x));
}

took other generic functions as arguments, like so:

ts
function arrayify<T>(x: T): T[] {
return [x];
}
type Box<U> = { value: U };
function boxify<U>(y: U): Box<U> {
return { value: y };
}
let newFn = compose(arrayify, boxify);

Instead of a relatively useless type like (x: {}) => Box<{}[]>, which older versions of the language would infer, TypeScript 3.4’s inference allows newFn to be generic. Its new type is <T>(x: T) => Box<T[]>.

TypeScript 3.5 generalizes this behavior to work on constructor functions as well.

ts
class Box<T> {
kind: "box";
value: T;
constructor(value: T) {
this.value = value;
}
}
class Bag<U> {
kind: "bag";
value: U;
constructor(value: U) {
this.value = value;
}
}
function composeCtor<T, U, V>(
F: new (x: T) => U,
G: new (y: U) => V
): (x: T) => V {
return (x) => new G(new F(x));
}
let f = composeCtor(Box, Bag); // has type '<T>(x: T) => Bag<Box<T>>'
let a = f(1024); // has type 'Bag<Box<number>>'

In addition to compositional patterns like the above, this new inference on generic constructors means that functions that operate on class components in certain UI libraries like React can more correctly operate on generic class components.

ts
type ComponentClass<P> = new (props: P) => Component<P>;
declare class Component<P> {
props: P;
constructor(props: P);
}
declare function myHoc<P>(C: ComponentClass<P>): ComponentClass<P>;
type NestedProps<T> = { foo: number; stuff: T };
declare class GenericComponent<T> extends Component<NestedProps<T>> {}
// type is 'new <T>(props: NestedProps<T>) => Component<NestedProps<T>>'
const GenericComponent2 = myHoc(GenericComponent);

To learn more, check out the original pull request on GitHub.

TypeScript 3.4

Faster subsequent builds with the --incremental flag

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
{
"": true,
"": "./lib"
},
"": ["./src"]
}

By default with these settings, when we run tsc, TypeScript will look for a file called .tsbuildinfo in the 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
{
"": true,
"": "./buildcache/front-end",
"": "./lib"
},
"": ["./src"]
}

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.

Higher order type inference from generic functions

TypeScript 3.4 can now produce generic function types when inference from other generic functions produces free type variables for inferences. This means many function composition patterns now work better in 3.4.

To get more specific, let’s build up some motivation and consider the following compose function:

ts
function compose<A, B, C>(f: (arg: A) => B, g: (arg: B) => C): (arg: A) => C {
return (x) => g(f(x));
}

compose takes two other functions:

  • f which takes some argument (of type A) and returns a value of type B
  • g which takes an argument of type B (the type f returned), and returns a value of type C

compose then returns a function which feeds its argument through f and then g.

When calling this function, TypeScript will try to figure out the types of A, B, and C through a process called type argument inference. This inference process usually works pretty well:

ts
interface Person {
name: string;
age: number;
}
function getDisplayName(p: Person) {
return p.name.toLowerCase();
}
function getLength(s: string) {
return s.length;
}
// has type '(p: Person) => number'
const getDisplayNameLength = compose(getDisplayName, getLength);
// works and returns the type 'number'
getDisplayNameLength({ name: "Person McPersonface", age: 42 });

The inference process is fairly straightforward here because getDisplayName and getLength use types that can easily be referenced. However, in TypeScript 3.3 and earlier, generic functions like compose didn’t work so well when passed other generic functions.

ts
interface Box<T> {
value: T;
}
function makeArray<T>(x: T): T[] {
return [x];
}
function makeBox<U>(value: U): Box<U> {
return { value };
}
// has type '(arg: {}) => Box<{}[]>'
const makeBoxedArray = compose(makeArray, makeBox);
makeBoxedArray("hello!").value[0].toUpperCase();
// ~~~~~~~~~~~
// error: Property 'toUpperCase' does not exist on type '{}'.

In older versions, TypeScript would infer the empty object type ({}) when inferring from other type variables like T and U.

During type argument inference in TypeScript 3.4, for a call to a generic function that returns a function type, TypeScript will, as appropriate, propagate type parameters from generic function arguments onto the resulting function type.

In other words, instead of producing the type

(arg: {}) => Box<{}[]>

TypeScript 3.4 produces the type

ts
<T>(arg: T) => Box<T[]>

Notice that T has been propagated from makeArray into the resulting type’s type parameter list. This means that genericity from compose’s arguments has been preserved and our makeBoxedArray sample will just work!

ts
interface Box<T> {
value: T;
}
function makeArray<T>(x: T): T[] {
return [x];
}
function makeBox<U>(value: U): Box<U> {
return { value };
}
// has type '<T>(arg: T) => Box<T[]>'
const makeBoxedArray = compose(makeArray, makeBox);
// works with no problem!
makeBoxedArray("hello!").value[0].toUpperCase();

For more details, you can read more at the original change.

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 reference to a ReadonlyArray can’t add, remove, or replace any elements of the array.

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

While it’s good practice to use ReadonlyArray over Array when no mutation is intended, 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.

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

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

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

ts
// 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.

ts
// 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.

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

ts
let err1: readonly Set<number>; // error!
let err2: readonly Array<boolean>; // error!
let okay: readonly boolean[]; // works fine

You can see more details in the pull request.

const assertions

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
ts
// Type '"hello"'
let x = "hello" 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.

ts
// Type '"hello"'
let x = <const>"hello";
// Type 'readonly [10, 20]'
let y = <const>[10, 20];
// Type '{ readonly text: "hello" }'
let z = <const>{ text: "hello" };

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

ts
// 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.

This can even be used to enable enum-like patterns in plain JavaScript code if you choose not to use TypeScript’s enum construct.

ts
export const Colors = {
red: "RED",
blue: "BLUE",
green: "GREEN",
} as const;
// or use an 'export default'
export default {
red: "RED",
blue: "BLUE",
green: "GREEN",
} as const;

Caveats

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

ts
// Error! A 'const' assertion can only be applied to a
// 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.

ts
let arr = [1, 2, 3, 4];
let foo = {
name: "foo",
contents: arr,
} as const;
foo.name = "bar"; // error!
foo.contents = []; // error!
foo.contents.push(5); // ...works!

For more details, you can check out the respective pull request.

Type-checking for globalThis

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.

ts
// in a global file:
var abc = 100;
// Refers to 'abc' from above.
globalThis.abc = 200;

Note that global variables declared with let and const don’t show up on globalThis.

ts
let answer = 42;
// error! Property 'answer' does not exist on 'typeof globalThis'.
globalThis.answer = 333333;

It’s also 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.

For more details on the implementation, see the feature’s pull request.

TypeScript 3.3

Improved behavior for calling union types

In prior versions of TypeScript, unions of callable types could only be invoked if they had identical parameter lists.

ts
type Fruit = "apple" | "orange";
type Color = "red" | "orange";
type FruitEater = (fruit: Fruit) => number; // eats and ranks the fruit
type ColorConsumer = (color: Color) => string; // consumes and describes the colors
declare let f: FruitEater | ColorConsumer;
// Cannot invoke an expression whose type lacks a call signature.
// Type 'FruitEater | ColorConsumer' has no compatible call signatures.ts(2349)
f("orange");

However, in the above example, both FruitEaters and ColorConsumers should be able to take the string "orange", and return either a number or a string.

In TypeScript 3.3, this is no longer an error.

ts
type Fruit = "apple" | "orange";
type Color = "red" | "orange";
type FruitEater = (fruit: Fruit) => number; // eats and ranks the fruit
type ColorConsumer = (color: Color) => string; // consumes and describes the colors
declare let f: FruitEater | ColorConsumer;
f("orange"); // It works! Returns a 'number | string'.
f("apple"); // error - Argument of type '"red"' is not assignable to parameter of type '"orange"'.
f("red"); // error - Argument of type '"red"' is not assignable to parameter of type '"orange"'.

In TypeScript 3.3, the parameters of these signatures are intersected together to create a new signature.

In the example above, the parameters fruit and color are intersected together to a new parameter of type Fruit & Color. Fruit & Color is really the same as ("apple" | "orange") & ("red" | "orange") which is equivalent to ("apple" & "red") | ("apple" & "orange") | ("orange" & "red") | ("orange" & "orange"). Each of those impossible intersections reduces to never, and we’re left with "orange" & "orange" which is just "orange".

Caveats

This new behavior only kicks in when at most one type in the union has multiple overloads, and at most one type in the union has a generic signature. That means methods on number[] | string[] like map (which is generic) still won’t be callable.

On the other hand, methods like forEach will now be callable, but under noImplicitAny there may be some issues.

ts
interface Dog {
kind: "dog";
dogProp: any;
}
interface Cat {
kind: "cat";
catProp: any;
}
const catOrDogArray: Dog[] | Cat[] = [];
catOrDogArray.forEach((animal) => {
// ~~~~~~ error!
// Parameter 'animal' implicitly has an 'any' type.
});

This is still strictly more capable in TypeScript 3.3, and adding an explicit type annotation will work.

ts
interface Dog {
kind: "dog";
dogProp: any;
}
interface Cat {
kind: "cat";
catProp: any;
}
const catOrDogArray: Dog[] | Cat[] = [];
catOrDogArray.forEach((animal: Dog | Cat) => {
if (animal.kind === "dog") {
animal.dogProp;
// ...
} else if (animal.kind === "cat") {
animal.catProp;
// ...
}
});

Incremental file watching for composite projects in --build --watch

TypeScript 3.0 introduced a new feature for structuring builds called “composite projects”. Part of the goal here was to ensure users could break up large projects into smaller parts that build quickly and preserve project structure, without compromising the existing TypeScript experience. Thanks to composite projects, TypeScript can use --build mode to recompile only the set of projects and dependencies. You can think of this as optimizing inter-project builds.

TypeScript 2.7 also introduced --watch mode builds via a new incremental “builder” API. In a similar vein, the entire idea is that this mode only re-checks and re-emits changed files or files whose dependencies might impact type-checking. You can think of this as optimizing intra-project builds.

Prior to 3.3, building composite projects using --build --watch actually didn’t use this incremental file watching infrastructure. An update in one project under --build --watch mode would force a full build of that project, rather than determining which files within that project were affected.

In TypeScript 3.3, --build mode’s --watch flag does leverage incremental file watching as well. That can mean signficantly faster builds under --build --watch. In our testing, this functionality has resulted in a reduction of 50% to 75% in build times of the original --build --watch times. You can read more on the original pull request for the change to see specific numbers, but we believe most composite project users will see significant wins here.

TypeScript 3.2

strictBindCallApply

TypeScript 3.2 introduces a new --strictBindCallApply compiler option (in the --strict family of options) with which the bind, call, and apply methods on function objects are strongly typed and strictly checked.

ts
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

This is achieved by introducing two new types, CallableFunction and NewableFunction, in lib.d.ts. These types contain specialized generic method declarations for bind, call, and apply for regular functions and constructor functions, respectively. The declarations use generic rest parameters (see #24897) to capture and reflect parameter lists in a strongly typed manner. In --strictBindCallApply mode these declarations are used in place of the (very permissive) declarations provided by type Function.

Caveats

Since the stricter checks may uncover previously unreported errors, this is a breaking change in --strict mode.

Additionally, another 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.

Generic spread expressions in object literals

In TypeScript 3.2, object literals now allow generic spread expressions which now produce intersection types, similar to the Object.assign function and JSX literals. For example:

ts
function taggedObject<T, U extends string>(obj: T, tag: U) {
return { ...obj, tag }; // T & { tag: U }
}
let x = taggedObject({ x: 10, y: 20 }, "point"); // { x: number, y: number } & { tag: "point" }

Property assignments and non-generic spread expressions are merged to the greatest extent possible on either side of a generic spread expression. For example:

ts
function foo1<T>(t: T, obj1: { a: string }, obj2: { b: string }) {
return { ...obj1, x: 1, ...t, ...obj2, y: 2 }; // { a: string, x: number } & T & { b: string, y: number }
}

Non-generic spread expressions continue to be processed as before: Call and construct signatures are stripped, only non-method properties are preserved, and for properties with the same name, the type of the rightmost property is used. This contrasts with intersection types which concatenate call and construct signatures, preserve all properties, and intersect the types of properties with the same name. Thus, spreads of the same types may produce different results when they are created through instantiation of generic types:

ts
function spread<T, U>(t: T, u: U) {
return { ...t, ...u }; // T & U
}
declare let x: { a: string; b: number };
declare let y: { b: string; c: boolean };
let s1 = { ...x, ...y }; // { a: string, b: string, c: boolean }
let s2 = spread(x, y); // { a: string, b: number } & { b: string, c: boolean }
let b1 = s1.b; // string
let b2 = s2.b; // number & string

Generic object rest variables and parameters

TypeScript 3.2 also allows destructuring a rest binding from a generic variable. This is achieved by using the predefined Pick and Exclude helper types from lib.d.ts, and using the generic type in question as well as the names of the other bindings in the destructuring pattern.

ts
function excludeTag<T extends { tag: string }>(obj: T) {
let { tag, ...rest } = obj;
return rest; // Pick<T, Exclude<keyof T, "tag">>
}
const taggedPoint = { x: 10, y: 20, tag: "point" };
const point = excludeTag(taggedPoint); // { x: number, y: number }

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:

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

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

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

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

Non-unit types as union discriminants

TypeScript 3.2 makes narrowing easier by relaxing rules for what it considers 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.

ts
type Result<T> = { error: Error; data: null } | { error: null; data: T };
function unwrap<T>(result: Result<T>) {
if (result.error) {
// Here 'error' is non-null
throw result.error;
}
// Now 'data' is non-null
return result.data;
}

tsconfig.json inheritance via Node.js packages

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.

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

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.

The new --showConfig flag

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.

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

js
// @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.

TypeScript 3.1

Mapped types on tuples and arrays

In TypeScript 3.1, mapped object types[1] over tuples and arrays now produce new tuples/arrays, rather than creating a new type where members like push(), pop(), and length are converted. For example:

ts
type MapToPromise<T> = { [K in keyof T]: Promise<T[K]> };
type Coordinate = [number, number];
type PromiseCoordinate = MapToPromise<Coordinate>; // [Promise<number>, Promise<number>]

MapToPromise takes a type T, and when that type is a tuple like Coordinate, only the numeric properties are converted. In [number, number], there are two numerically named properties: 0 and 1. When given a tuple like that, MapToPromise will create a new tuple where the 0 and 1 properties are Promises of the original type. So the resulting type PromiseCoordinate ends up with the type [Promise<number>, Promise<number>].

Properties declarations on functions

TypeScript 3.1 brings the ability to define properties on function declarations and const-declared functons, simply by assigning to properties on these functions in the same scope. This allows us to write canonical JavaScript code without resorting to namespace hacks. For example:

ts
function readImage(path: string, callback: (err: any, image: Image) => void) {
// ...
}
readImage.sync = (path: string) => {
const contents = fs.readFileSync(path);
return decodeImageSync(contents);
};

Here, we have a function readImage which reads an image in a non-blocking asynchronous way. In addition to readImage, we’ve provided a convenience function on readImage itself called readImage.sync.

While ECMAScript exports are often a better way of providing this functionality, this new support allows code written in this style to “just work” TypeScript. Additionaly, this approach for property declarations allows us to express common patterns like defaultProps and propTypes on React stateless function components (SFCs).

ts
export const FooComponent = ({ name }) => <div>Hello! I am {name}</div>;
FooComponent.defaultProps = {
name: "(anonymous)",
};

[1] More specifically, homomorphic mapped types like in the above form.

Version selection with typesVersions

Feedback from our community, as well as our own experience, has shown us that leveraging the newest TypeScript features while also accomodating users on the older versions are difficult. TypeScript introduces a new feature called typesVersions to help accomodate these scenarios.

When using Node module resolution in TypeScript 3.1, when TypeScript cracks open a package.json file to figure out which files it needs to read, it first looks at a new field called typesVersions. A package.json with a typesVersions field might look like this:

{
"name": "package-name",
"version": "1.0",
"": "./index.d.ts",
"typesVersions": {
">=3.1": { "*": ["ts3.1/*"] }
}
}

This package.json tells TypeScript to check whether the current version of TypeScript is running. If it’s 3.1 or later, it figures out the path you’ve imported relative to the package, and reads from the package’s ts3.1 folder. That’s what that { "*": ["ts3.1/*"] } means - if you’re familiar with path mapping today, it works exactly like that.

So in the above example, if we’re importing from "package-name", we’ll try to resolve from [...]/node_modules/package-name/ts3.1/index.d.ts (and other relevant paths) when running in TypeScript 3.1. If we import from package-name/foo, we’ll try to look for [...]/node_modules/package-name/ts3.1/foo.d.ts and [...]/node_modules/package-name/ts3.1/foo/index.d.ts.

What if we’re not running in TypeScript 3.1 in this example? Well, if none of the fields in typesVersions get matched, TypeScript falls back to the types field, so here TypeScript 3.0 and earlier will be redirected to [...]/node_modules/package-name/index.d.ts.

Matching behavior

The way that TypeScript decides on whether a version of the compiler & language matches is by using Node’s semver ranges.

Multiple fields

typesVersions can support multiple fields where each field name is specified by the range to match on.

{
"name": "package-name",
"version": "1.0",
"": "./index.d.ts",
"typesVersions": {
">=3.2": { "*": ["ts3.2/*"] },
">=3.1": { "*": ["ts3.1/*"] }
}
}

Since ranges have the potential to overlap, determining which redirect applies is order-specific. That means in the above example, even though both the >=3.2 and the >=3.1 matchers support TypeScript 3.2 and above, reversing the order could have different behavior, so the above sample would not be equivalent to the following.

{
"name": "package-name",
"version": "1.0",
"": "./index.d.ts",
"typesVersions": {
// NOTE: this doesn't work!
">=3.1": { "*": ["ts3.1/*"] },
">=3.2": { "*": ["ts3.2/*"] }
}
}

TypeScript 3.0

Tuples in rest parameters and spread expressions

TypeScript 3.0 adds support to multiple new capabilities to interact with function parameter lists as tuple types. TypeScript 3.0 adds support for:

With these features it becomes possible to strongly type a number of higher-order functions that transform functions and their parameter lists.

Rest parameters with tuple types

When a rest parameter has a tuple type, the tuple type is expanded into a sequence of discrete parameters. For example the following two declarations are equivalent:

ts
declare function foo(...args: [number, string, boolean]): void;
ts
declare function foo(args_0: number, args_1: string, args_2: boolean): void;

Spread expressions with tuple types

When a function call includes a spread expression of a tuple type as the last argument, the spread expression corresponds to a sequence of discrete arguments of the tuple element types.

Thus, the following calls are equivalent:

ts
const args: [number, string, boolean] = [42, "hello", true];
foo(42, "hello", true);
foo(args[0], args[1], args[2]);
foo(...args);

Generic rest parameters

A rest parameter is permitted to have a generic type that is constrained to an array type, and type inference can infer tuple types for such generic rest parameters. This enables higher-order capturing and spreading of partial parameter lists:

Example
ts
declare function bind<T, U extends any[], V>(
f: (x: T, ...args: U) => V,
x: T
): (...args: U) => V;
declare function f3(x: number, y: string, z: boolean): void;
const f2 = bind(f3, 42); // (y: string, z: boolean) => void
const f1 = bind(f2, "hello"); // (z: boolean) => void
const f0 = bind(f1, true); // () => void
f3(42, "hello", true);
f2("hello", true);
f1(true);
f0();

In the declaration of f2 above, type inference infers types number, [string, boolean] and void for T, U and V respectively.

Note that when a tuple type is inferred from a sequence of parameters and later expanded into a parameter list, as is the case for U, the original parameter names are used in the expansion (however, the names have no semantic meaning and are not otherwise observable).

Optional elements in tuple types

Tuple types now permit a ? postfix on element types to indicate that the element is optional:

Example
ts
let t: [number, string?, boolean?];
t = [42, "hello", true];
t = [42, "hello"];
t = [42];

In --strictNullChecks mode, a ? modifier automatically includes undefined in the element type, similar to optional parameters.

A tuple type permits an element to be omitted if it has a postfix ? modifier on its type and all elements to the right of it also have ? modifiers.

When tuple types are inferred for rest parameters, optional parameters in the source become optional tuple elements in the inferred type.

The length property of a tuple type with optional elements is a union of numeric literal types representing the possible lengths. For example, the type of the length property in the tuple type [number, string?, boolean?] is 1 | 2 | 3.

Rest elements in tuple types

The last element of a tuple type can be a rest element of the form ...X, where X is an array type. A rest element indicates that the tuple type is open-ended and may have zero or more additional elements of the array element type. For example, [number, ...string[]] means tuples with a number element followed by any number of string elements.

Example
ts
function tuple<T extends any[]>(...args: T): T {
return args;
}
const numbers: number[] = getArrayOfNumbers();
const t1 = tuple("foo", 1, true); // [string, number, boolean]
const t2 = tuple("bar", ...numbers); // [string, ...number[]]

The type of the length property of a tuple type with a rest element is number.

New unknown top type

TypeScript 3.0 introduces a new top type unknown. unknown is the type-safe counterpart of any. Anything is assignable to unknown, but unknown isn’t assignable to anything but itself and any without a type assertion or a control flow based narrowing. Likewise, no operations are permitted on an unknown without first asserting or narrowing to a more specific type.

Example
ts
// In an intersection everything absorbs unknown
type T00 = unknown & null; // null
type T01 = unknown & undefined; // undefined
type T02 = unknown & null & undefined; // null & undefined (which becomes never)
type T03 = unknown & string; // string
type T04 = unknown & string[]; // string[]
type T05 = unknown & unknown; // unknown
type T06 = unknown & any; // any
// In a union an unknown absorbs everything
type T10 = unknown | null; // unknown
type T11 = unknown | undefined; // unknown
type T12 = unknown | null | undefined; // unknown
type T13 = unknown | string; // unknown
type T14 = unknown | string[]; // unknown
type T15 = unknown | unknown; // unknown
type T16 = unknown | any; // any
// Type variable and unknown in union and intersection
type T20<T> = T & {}; // T & {}
type T21<T> = T | {}; // T | {}
type T22<T> = T & unknown; // T
type T23<T> = T | unknown; // unknown
// unknown in conditional types
type T30<T> = unknown extends T ? true : false; // Deferred
type T31<T> = T extends unknown ? true : false; // Deferred (so it distributes)
type T32<T> = never extends T ? true : false; // true
type T33<T> = T extends never ? true : false; // Deferred
// keyof unknown
type T40 = keyof any; // string | number | symbol
type T41 = keyof unknown; // never
// Only equality operators are allowed with unknown
function f10(x: unknown) {
x == 5;
x !== 10;
x >= 0; // Error
x + 1; // Error
x * 2; // Error
-x; // Error
+x; // Error
}
// No property accesses, element accesses, or function calls
function f11(x: unknown) {
x.foo; // Error
x[5]; // Error
x(); // Error
new x(); // Error
}
// typeof, instanceof, and user defined type predicates
declare function isFunction(x: unknown): x is Function;
function f20(x: unknown) {
if (typeof x === "string" || typeof x === "number") {
x; // string | number
}
if (x instanceof Error) {
x; // Error
}
if (isFunction(x)) {
x; // Function
}
}
// Homomorphic mapped type over unknown
type T50<T> = { [P in keyof T]: number };
type T51 = T50<any>; // { [x: string]: number }
type T52 = T50<unknown>; // {}
// Anything is assignable to unknown
function f21<T>(pAny: any, pNever: never, pT: T) {
let x: unknown;
x = 123;
x = "hello";
x = [1, 2, 3];
x = new Error();
x = x;
x = pAny;
x = pNever;
x = pT;
}
// unknown assignable only to itself and any
function f22(x: unknown) {
let v1: any = x;
let v2: unknown = x;
let v3: object = x; // Error
let v4: string = x; // Error
let v5: string[] = x; // Error
let v6: {} = x; // Error
let v7: {} | null | undefined = x; // Error
}
// Type parameter 'T extends unknown' not related to object
function f23<T extends unknown>(x: T) {
let y: object = x; // Error
}
// Anything but primitive assignable to { [x: string]: unknown }
function f24(x: { [x: string]: unknown }) {
x = {};
x = { a: 5 };
x = [1, 2, 3];
x = 123; // Error
}
// Locals of type unknown always considered initialized
function f25() {
let x: unknown;
let y = x;
}
// Spread of unknown causes result to be unknown
function f26(x: {}, y: unknown, z: any) {
let o1 = { a: 42, ...x }; // { a: number }
let o2 = { a: 42, ...x, ...y }; // unknown
let o3 = { a: 42, ...x, ...y, ...z }; // any
}
// Functions with unknown return type don't need return expressions
function f27(): unknown {}
// Rest type cannot be created from unknown
function f28(x: unknown) {
let { ...a } = x; // Error
}
// Class properties of type unknown don't need definite assignment
class C1 {
a: string; // Error
b: unknown;
c: any;
}

Support for defaultProps in JSX

TypeScript 2.9 and earlier didn’t leverage React defaultProps declarations inside JSX components. Users would often have to declare properties optional and use non-null assertions inside of render, or they’d use type-assertions to fix up the type of the component before exporting it.

TypeScript 3.0 adds supports a new type alias in the JSX namespace called LibraryManagedAttributes. This helper type defines a transformation on the component’s Props type, before using to check a JSX expression targeting it; thus allowing customization like: how conflicts between provided props and inferred props are handled, how inferences are mapped, how optionality is handled, and how inferences from differing places should be combined.

In short using this general type, we can model React’s specific behavior for things like defaultProps and, to some extent, propTypes.

tsx
export interface Props {
name: string;
}
export class Greet extends React.Component<Props> {
render() {
const { name } = this.props;
return <div>Hello ${name.toUpperCase()}!</div>;
}
static defaultProps = { name: "world" };
}
// Type-checks! No type assertions needed!
let el = <Greet />;

Caveats

Explicit types on defaultProps

The default-ed properties are inferred from the defaultProps property type. If an explicit type annotation is added, e.g. static defaultProps: Partial<Props>; the compiler will not be able to identify which properties have defaults (since the type of defaultProps include all properties of Props).

Use static defaultProps: Pick<Props, "name">; as an explicit type annotation instead, or do not add a type annotation as done in the example above.

For stateless function components (SFCs) use ES2015 default initializers for SFCs:

tsx
function Greet({ name = "world" }: Props) {
return <div>Hello ${name.toUpperCase()}!</div>;
}
Changes to @types/React

Corresponding changes to add LibraryManagedAttributes definition to the JSX namespace in @types/React are still needed. Keep in mind that there are some limitations.

/// <reference lib="..." /> reference directives

TypeScript adds a new triple-slash-reference directive (/// <reference lib="name" />), allowing a file to explicitly include an existing built-in lib file.

Built-in lib files are referenced in the same fashion as the "lib" compiler option in tsconfig.json (e.g. use lib="es2015" and not lib="lib.es2015.d.ts", etc.).

For declaration file authors who rely on built-in types, e.g. DOM APIs or built-in JS run-time constructors like Symbol or Iterable, triple-slash-reference lib directives are the recommended. Previously these .d.ts files had to add forward/duplicate declarations of such types.

Example

Using /// <reference lib="es2017.string" /> to one of the files in a compilation is equivalent to compiling with --lib es2017.string.

ts
/// <reference lib="es2017.string" />
"foo".padStart(4);

TypeScript 2.9

Support number and symbol named properties with keyof and mapped types

TypeScript 2.9 adds support for number and symbol named properties in index types and mapped types. Previously, the keyof operator and mapped types only supported string named properties.

Changes include:

  • An index type keyof T for some type T is a subtype of string | number | symbol.
  • A mapped type { [P in K]: XXX } permits any K assignable to string | number | symbol.
  • In a for...in statement for an object of a generic type T, the inferred type of the iteration variable was previously keyof T but is now Extract<keyof T, string>. (In other words, the subset of keyof T that includes only string-like values.)

Given an object type X, keyof X is resolved as follows:

  • If X contains a string index signature, keyof X is a union of string, number, and the literal types representing symbol-like properties, otherwise
  • If X contains a numeric index signature, keyof X is a union of number and the literal types representing string-like and symbol-like properties, otherwise
  • keyof X is a union of the literal types representing string-like, number-like, and symbol-like properties.

Where:

  • String-like properties of an object type are those declared using an identifier, a string literal, or a computed property name of a string literal type.
  • Number-like properties of an object type are those declared using a numeric literal or computed property name of a numeric literal type.
  • Symbol-like properties of an object type are those declared using a computed property name of a unique symbol type.

In a mapped type { [P in K]: XXX }, each string literal type in K introduces a property with a string name, each numeric literal type in K introduces a property with a numeric name, and each unique symbol type in K introduces a property with a unique symbol name. Furthermore, if K includes type string, a string index signature is introduced, and if K includes type number, a numeric index signature is introduced.

Example

ts
const c = "c";
const d = 10;
const e = Symbol();
const enum E1 {
A,
B,
C,
}
const enum E2 {
A = "A",
B = "B",
C = "C",
}
type Foo = {
a: string; // String-like name
5: string; // Number-like name
[c]: string; // String-like name
[d]: string; // Number-like name
[e]: string; // Symbol-like name
[E1.A]: string; // Number-like name
[E2.A]: string; // String-like name
};
type K1 = keyof Foo; // "a" | 5 | "c" | 10 | typeof e | E1.A | E2.A
type K2 = Extract<keyof Foo, string>; // "a" | "c" | E2.A
type K3 = Extract<keyof Foo, number>; // 5 | 10 | E1.A
type K4 = Extract<keyof Foo, symbol>; // typeof e

Since keyof now reflects the presence of a numeric index signature by including type number in the key type, mapped types such as Partial<T> and Readonly<T> work correctly when applied to object types with numeric index signatures:

ts
type Arrayish<T> = {
length: number;
[x: number]: T;
};
type ReadonlyArrayish<T> = Readonly<Arrayish<T>>;
declare const map: ReadonlyArrayish<string>;
let n = map.length;
let x = map[123]; // Previously of type any (or an error with --noImplicitAny)

Furthermore, with the keyof operator’s support for number and symbol named keys, it is now possible to abstract over access to properties of objects that are indexed by numeric literals (such as numeric enum types) and unique symbols.

ts
const enum Enum {
A,
B,
C,
}
const enumToStringMap = {
[Enum.A]: "Name A",
[Enum.B]: "Name B",
[Enum.C]: "Name C",
};
const sym1 = Symbol();
const sym2 = Symbol();
const sym3 = Symbol();
const symbolToNumberMap = {
[sym1]: 1,
[sym2]: 2,
[sym3]: 3,
};
type KE = keyof typeof enumToStringMap; // Enum (i.e. Enum.A | Enum.B | Enum.C)
type KS = keyof typeof symbolToNumberMap; // typeof sym1 | typeof sym2 | typeof sym3
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let x1 = getValue(enumToStringMap, Enum.C); // Returns "Name C"
let x2 = getValue(symbolToNumberMap, sym3); // Returns 3

This is a breaking change; previously, the keyof operator and mapped types only supported string named properties. Code that assumed values typed with keyof T were always strings, will now be flagged as error.

Example

ts
function useKey<T, K extends keyof T>(o: T, k: K) {
var name: string = k; // Error: keyof T is not assignable to string
}

Recommendations

  • If your functions are only able to handle string named property keys, use Extract<keyof T, string> in the declaration:

    ts
    function useKey<T, K extends Extract<keyof T, string>>(o: T, k: K) {
    var name: string = k; // OK
    }
  • If your functions are open to handling all property keys, then the changes should be done down-stream:

    ts
    function useKey<T, K extends keyof T>(o: T, k: K) {
    var name: string | number | symbol = k;
    }
  • Otherwise use --keyofStringsOnly compiler option to disable the new behavior.

Generic type arguments in JSX elements

JSX elements now allow passing type arguments to generic components.

Example

ts
class GenericComponent<P> extends React.Component<P> {
internalProp: P;
}
type Props = { a: number; b: string };
const x = <GenericComponent<Props> a={10} b="hi" />; // OK
const y = <GenericComponent<Props> a={10} b={20} />; // Error

Generic type arguments in generic tagged templates

Tagged templates are a form of invocation introduced in ECMAScript 2015. Like call expressions, generic functions may be used in a tagged template and TypeScript will infer the type arguments utilized.

TypeScript 2.9 allows passing generic type arguments to tagged template strings.

Example

ts
declare function styledComponent<Props>(
strs: TemplateStringsArray
): Component<Props>;
interface MyProps {
name: string;
age: number;
}
styledComponent<MyProps>`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
declare function tag<T>(strs: TemplateStringsArray, ...args: T[]): T;
// inference fails because 'number' and 'string' are both candidates that conflict
let a = tag<string | number>`${100} ${"hello"}`;

import types

Modules can import types declared in other modules. But non-module global scripts cannot access types declared in modules. Enter import types.

Using import("mod") in a type annotation allows for reaching in a module and accessing its exported declaration without importing it.

Example

Given a declaration of a class Pet in a module file:

ts
// module.d.ts
export declare class Pet {
name: string;
}

Can be used in a non-module file global-script.ts:

ts
// global-script.ts
function adopt(p: import("./module").Pet) {
console.log(`Adopting ${p.name}...`);
}

This also works in JSDoc comments to refer to types from other modules in .js:

js
// a.js
/**
* @param p { import("./module").Pet }
*/
function walk(p) {
console.log(`Walking ${p.name}...`);
}

Relaxing declaration emit visiblity rules

With import types available, many of the visibility errors reported during declaration file generation can be handled by the compiler without the need to change the input.

For instance:

ts
import { createHash } from "crypto";
export const hash = createHash("sha256");
// ^^^^
// Exported variable 'hash' has or is using name 'Hash' from external module "crypto" but cannot be named.

With TypeScript 2.9, no errors are reported, and now the generated file looks like:

ts
export declare const hash: import("crypto").Hash;

Support for import.meta

TypeScript 2.9 introduces support for import.meta, a new meta-property as described by the current TC39 proposal.

The type of import.meta is the global ImportMeta type which is defined in lib.es5.d.ts. This interface is extremely limited. Adding well-known properties for Node or browsers requires interface merging and possibly a global augmentation depending on the context.

Example

Assuming that __dirname is always available on import.meta, the declaration would be done through reopening ImportMeta interface:

ts
// node.d.ts
interface ImportMeta {
__dirname: string;
}

And usage would be:

ts
import.meta.__dirname; // Has type 'string'

import.meta is only allowed when targeting ESNext modules and ECMAScript targets.

New --resolveJsonModule

Often in Node.js applications a .json is needed. With TypeScript 2.9, --resolveJsonModule allows for importing, extracting types from and generating .json files.

Example

ts
// settings.json
{
"repo": "TypeScript",
"dry": false,
"debug": false
}
ts
// a.ts
import settings from "./settings.json";
settings.debug === true; // OK
settings.dry === 2; // Error: Operator '===' cannot be applied boolean and number
{
"": "commonjs",
}
}

--pretty output by default

Starting TypeScript 2.9 errors are displayed under --pretty by default if the output device is applicable for colorful text. TypeScript will check if the output steam has isTty property set.

Use --pretty false on the command line or set "pretty": false in your tsconfig.json to disable --pretty output.

New --declarationMap

Enabling --declarationMap alongside --declaration causes the compiler to emit .d.ts.map files alongside the output .d.ts files. Language Services can also now understand these map files, and uses them to map declaration-file based definition locations to their original source, when available.

In other words, hitting go-to-definition on a declaration from a .d.ts file generated with --declarationMap will take you to the source file (.ts) location where that declaration was defined, and not to the .d.ts.

TypeScript 2.8

Conditional Types

TypeScript 2.8 introduces conditional types which add the ability to express non-uniform type mappings. A conditional type selects one of two possible types based on a condition expressed as a type relationship test:

ts
T extends U ? X : Y

The type above means when T is assignable to U the type is X, otherwise the type is Y.

A conditional type T extends U ? X : Y is either resolved to X or Y, or deferred because the condition depends on one or more type variables. Whether to resolve or defer is determined as follows:

  • First, given types T' and U' that are instantiations of T and U where all occurrences of type parameters are replaced with any, if T' is not assignable to U', the conditional type is resolved to Y. Intuitively, if the most permissive instantiation of T is not assignable to the most permissive instantiation of U, we know that no instantiation will be and we can just resolve to Y.
  • Next, for each type variable introduced by an infer (more later) declaration within U collect a set of candidate types by inferring from T to U (using the same inference algorithm as type inference for generic functions). For a given infer type variable V, if any candidates were inferred from co-variant positions, the type inferred for V is a union of those candidates. Otherwise, if any candidates were inferred from contra-variant positions, the type inferred for V is an intersection of those candidates. Otherwise, the type inferred for V is never.
  • Then, given a type T'' that is an instantiation of T where all infer type variables are replaced with the types inferred in the previous step, if T'' is definitely assignable to U, the conditional type is resolved to X. The definitely assignable relation is the same as the regular assignable relation, except that type variable constraints are not considered. Intuitively, when a type is definitely assignable to another type, we know that it will be assignable for all instantiations of those types.
  • Otherwise, the condition depends on one or more type variables and the conditional type is deferred.

Example

ts
type TypeName<T> = T extends string
? "string"
: T extends number
? "number"
: T extends boolean
? "boolean"
: T extends undefined
? "undefined"
: T extends Function
? "function"
: "object";
type T0 = TypeName<string>; // "string"
type T1 = TypeName<"a">; // "string"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<string[]>; // "object"

Distributive conditional types

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

Example

ts
type T10 = TypeName<string | (() => void)>; // "string" | "function"
type T12 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined"
type T11 = TypeName<string[] | number[]>; // "object"

In instantiations of a distributive conditional type T extends U ? X : Y, references to T within the conditional type are resolved to individual constituents of the union type (i.e. T refers to the individual constituents after the conditional type is distributed over the union type). Furthermore, references to T within X have an additional type parameter constraint U (i.e. T is considered assignable to U within X).

Example

ts
type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;
type T20 = Boxed<string>; // BoxedValue<string>;
type T21 = Boxed<number[]>; // BoxedArray<number>;
type T22 = Boxed<string | number[]>; // BoxedValue<string> | BoxedArray<number>;

Notice that T has the additional constraint any[] within the true branch of Boxed<T> and it is therefore possible to refer to the element type of the array as T[number]. Also, notice how the conditional type is distributed over the union type in the last example.

The distributive property of conditional types can conveniently be used to filter union types:

ts
type Diff<T, U> = T extends U ? never : T; // Remove types from T that are assignable to U
type Filter<T, U> = T extends U ? T : never; // Remove types from T that are not assignable to U
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
type T32 = Diff<string | number | (() => void), Function>; // string | number
type T33 = Filter<string | number | (() => void), Function>; // () => void
type NonNullable<T> = Diff<T, null | undefined>; // Remove null and undefined from T
type T34 = NonNullable<string | number | undefined>; // string | number
type T35 = NonNullable<string | string[] | null | undefined>; // string | string[]
function f1<T>(x: T, y: NonNullable<T>) {
x = y; // Ok
y = x; // Error
}
function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
x = y; // Ok
y = x; // Error
let s1: string = x; // Error
let s2: string = y; // Ok
}

Conditional types are particularly useful when combined with mapped types:

ts
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface Part {
id: number;
name: string;
subparts: Part[];
updatePart(newName: string): void;
}
type T40 = FunctionPropertyNames<Part>; // "updatePart"
type T41 = NonFunctionPropertyNames<Part>; // "id" | "name" | "subparts"
type T42 = FunctionProperties<Part>; // { updatePart(newName: string): void }
type T43 = NonFunctionProperties<Part>; // { id: number, name: string, subparts: Part[] }

Similar to union and intersection types, conditional types are not permitted to reference themselves recursively. For example the following is an error.