Announcing TypeScript 3.9

Daniel

May 12th, 2020

Today we’re excited to announce the release of TypeScript 3.9!

If you’re unfamiliar with TypeScript, it’s a language that builds on JavaScript by adding syntax for type declarations and annotations. This syntax can be used by the TypeScript compiler to type-check our code, and then output clean readable JavaScript that runs on lots of different runtimes. Static type-checking can tell us about errors in our code before we even run it, or before we even save our files thanks to TypeScript’s rich editing functionality across editors. But beyond error-checking, TypeScript powers things like completions, quick fixes, and refactorings for both TypeScript and JavaScript in some of your favorite editors. In fact, if you already use Visual Studio or Visual Studio Code, you might already be using TypeScript when you write JavaScript code! So if you’re interested in learning more, check out our website!

But if you’re already using TypeScript in your project, you can either get it through NuGet or use npm with the following command:

For this release our team been has been focusing on performance, polish, and stability. We’ve been working on speeding up the compiler and editing experience, getting rid of friction and papercuts, and reducing bugs and crashes. We’ve also received a number of useful and much-appreciated features and fixes from the external community!

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.

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.

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.

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.

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

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

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.

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.

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

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

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.

// beforeletf1=()=>42// oops - not the same!letf2=()=>{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.

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.

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.

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

foo?.bar!.baz

was interpreted to be equivalent to the following JavaScript.

(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

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.

(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. <div> 2 &gt; 1 </div>) or insert an expression with a string literal (e.g. <div> 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 `&gt;`?
Unexpected token. Did you mean `{'}'}` or `&rbrace;`?

In previous versions of TypeScript, this was allowed because while A was totally incompatible with C, Bwas 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'.

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:

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.

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.

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, but bundlers may have a harder time tree-shaking the code.

Exports Now Use Getters for Live Bindings

When targeting module systems like CommonJS in ES5 and above, TypeScript will use get accessors to emulate live bindings so that changes to a variable in one module are witnessed in any exporting modules. This change is meant to make TypeScript’s emit more compliant with ECMAScript modules.

Exports are Hoisted and Initially Assigned

TypeScript now hoists exported declarations to the top of the file when targeting module systems like CommonJS in ES5 and above. This change is meant to make TypeScript’s emit more compliant with ECMAScript modules. For example, code like

export * from"mod";exportconstnameFromMod=0;

previously had output like

__exportStar(exports,require("mod"));exports.nameFromMod=0;

However, because exports now use get-accessors, this assignment would throw because __exportStar now makes get-accesors which can’t be overridden with a simple assignment. Instead, TypeScript 3.9 emits the following:

What’s Next?

We hope that TypeScript 3.9 makes your day-to-day coding fun, fast, and an overall joy to use. To stay in the loop on our next version, you can track the 4.0 Iteration Plan and our Feature Roadmap as it comes together.

For the past year, we’ve been working on tooling to protect the stability of the Definitely Typed ecosystem to the point where we can offer a more streamlined experience when contributing to DT repo – definition owners can now help merge PRs to their modules.

To each their own, but have you tried TypeScript? Anything in particular you don’t like?

Having started with JavaScript, and done PHP, ColdFusion, and finally C# work, when I came back to doing more with JavaScript I really wished I could bring over some of the intelligence of typed languages like Java and C#. With TypeScript I’m writing code that’s less likely to have errors, and is more understandable after I’ve left it for a while.

It doesn’t make sense for projects with small amounts of JavaScript, but I work with a couple projects with significant JavaScript functionality that I’d love to migrate over.