Was this page helpful?

TypeScript 3.9

Improvements in Inference andPromise.all

Recent versions of TypeScript (around 3.7) have had updates to the declarations of functions likePromise.all andPromise.race.Unfortunately, that introduced a few regressions, especially when mixing in values withnull orundefined.

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

This is strange behavior!The fact thatsealExhibit contained anundefined somehow poisoned type oflion to includeundefined.

Thanks toa pull request fromJack 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 aroundPromises, we encourage you to give 3.9 a shot!

What About theawaited Type?

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

We initially anticipated shippingawaited 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 bychanging 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 calleddoStuff as part of our public API.The function’s types declare that it takes twostrings 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
functiondoStuff(abc:string,xyz:string) {
assert(typeofabc ==="string");
assert(typeofxyz ==="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 preceded by 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 toJosh Goldberg, the contributor who implemented this feature.For more information, you can take a look atthets-expect-error pull request.

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

Pickts-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 as affected code is valid again

Pickts-ignore if:

  • you have a larger project 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 introduceduncalled function checks to report an error when you’ve forgotten to call a function.

ts
functionhasImportantPermissions():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 inif statements.Thanks toa pull request fromAlexander Tarasyuk, this feature is also now supported in ternary conditionals (i.e. thecond ? trueExpr : falseExpr syntax).

ts
declarefunctionlistFilesOfDirectory(dirPath:string):string[];
declarefunctionisDirectory():boolean;
functiongetAllFiles(startFileName:string) {
constresult:string[] = [];
traverse(startFileName);
returnresult;
functiontraverse(currentPath:string) {
returnisDirectory
?// ~~~~~~~~~~~
// 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*asfsfrom"fs";

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

js
constfs =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, seethe 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
constmaxValue =100;
/*start*/
for (leti =0;i <=maxValue;i++) {
// First get the squared value.
letsquare =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
constmaxValue =100;
printSquares();
functionprintSquares() {
for (leti =0;i <=maxValue;i++) {
// First get the squared value.
letsquare =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 ourfor loop, but the refactoring got rid of it!TypeScript 3.9 does a little more work to preserve what we write.

ts
constmaxValue =100;
printSquares();
functionprintSquares() {
for (leti =0;i <=maxValue;i++) {
// First get the squared value.
letsquare =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 implementationin 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
letf1 = ()=>42;
// oops - not the same!
letf2 = ()=> {
42;
};

Thanks toa pull request from community memberWenlu Wang, TypeScript can provide a quick-fix to add missingreturn 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 atsconfig.json.

One case where this slightly fell over is when atsconfig.json simply existed to reference othertsconfig.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 thesetsconfig.*.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 roottsconfig.json.

TypeScript 3.9 adds support to editing scenarios for this configuration.For more details, take a look atthe 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 iffoo isundefined, accessingbaz 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 removenull andundefined from the type ofbar.

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

js
foo?.bar.baz;

which just evaluates toundefined whenfoo isundefined.

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 conformant.The new way to insert these characters is to use an HTML escape code (e.g.<span> 2 &gt 1 </span>) or insert an expression with a string literal (e.g.<span> 2 {">"} 1 </span>).

Luckily, thanks to thepull request enforcing this fromBrad 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
letdirections =<span>Navigate to: Menu Bar > Tools > Options</span>;
// ~ ~
// Unexpected token. Did you mean `{'>'}` or `>`?

That error message came with a handy quick fix, and thanks toAlexander 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 likeA & B is assignable toC if eitherA orB is assignable toC; however, sometimes that has problems with optional properties.For example, take the following:

ts
interfaceA {
a:number;// notice this is 'number'
}
interfaceB {
b:string;
}
interfaceC {
a?:boolean;// notice this is 'boolean'
b:string;
}
declareletx:A &B;
declarelety:C;
y =x;

In previous versions of TypeScript, this was allowed because whileA was totally incompatible withC,Bwas compatible withC.

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 thea property ofA & B is incompatible with that ofC:

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
declarefunctionsmushObjects<T,U>(x:T,y:U):T &U;
interfaceCircle {
kind:"circle";
radius:number;
}
interfaceSquare {
kind:"square";
sideLength:number;
}
declareletx:Circle;
declarelety:Square;
letz =smushObjects(x,y);
console.log(z.kind);

This code is slightly weird because there’s really no way to create an intersection of aCircle and aSquare - they have two incompatiblekind fields.In previous versions of TypeScript, this code was allowed and the type ofkind itself wasnever because"circle" & "square" described a set of values that couldnever exist.

In TypeScript 3.9, the type system is more aggressive here - it notices that it’s impossible to intersectCircle andSquare because of theirkind properties.So instead of collapsing the type ofz.kind tonever, it collapses the type ofz itself (Circle & Square) tonever.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 andset 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 toa pull request from GitHub userpathurs, TypeScript 3.9 now conforms more closely with ECMAScript in this regard.

Type Parameters That Extendany No Longer Act asany

In previous versions of TypeScript, a type parameter constrained toany could be treated asany.

ts
functionfoo<Textendsany>(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
functionfoo<Textendsany>(arg:T) {
arg.spfjgerijghoied;
// ~~~~~~~~~~~~~~~
// Property 'spfjgerijghoied' does not exist on type 'T'.
}

export * is Always Retained

In previous TypeScript versions, declarations likeexport * from "foo" would be dropped in our JavaScript output iffoo 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 theseexport * declarations.In practice, we don’t expect this to break much existing code.

More libdom.d.ts refinements

We are continuing to move more of TypeScript’s built-in .d.ts library (lib.d.ts and family) to be generated from Web IDL files directly from the DOM specification.As a result some vendor-specific types related to media access have been removed.

Adding this file to an ambient*.d.ts to your project will bring them back:

ts
interfaceAudioTrackList {
[Symbol.iterator]():IterableIterator<AudioTrack>;
}
interfaceHTMLVideoElement {
readonlyaudioTracks:AudioTrackList
msFrameStep(forward:boolean):void;
msInsertVideoEffect(activatableClassId:string,effectRequired:boolean,config?:any):void;
msSetVideoRectangle(left:number,top:number,right:number,bottom:number):void;
webkitEnterFullScreen():void;
webkitEnterFullscreen():void;
webkitExitFullScreen():void;
webkitExitFullscreen():void;
msHorizontalMirror:boolean;
readonlymsIsLayoutOptimalForPlayback:boolean;
readonlymsIsStereo3D:boolean;
msStereo3DPackingMode:string;
msStereo3DRenderMode:string;
msZoom:boolean;
onMSVideoFormatChanged: ((this:HTMLVideoElement,ev:Event)=>any) |null;
onMSVideoFrameStepCompleted: ((this:HTMLVideoElement,ev:Event)=>any) |null;
onMSVideoOptimalLayoutChanged: ((this:HTMLVideoElement,ev:Event)=>any) |null;
webkitDisplayingFullscreen:boolean;
webkitSupportsFullscreen:boolean;
}
interfaceMediaError {
readonlymsExtendedCode:number;
readonlyMS_MEDIA_ERR_ENCRYPTED:number;
}

The TypeScript docs are an open source project. Help us improve these pagesby sending a Pull Request

Contributors to this page:
OTOrta Therox  (12)
SLShammel Lee  (1)
NSNick Schonning  (1)
MUMasato Urai  (1)
HKHomyee King  (1)
2+

Last updated: Dec 16, 2025