declare function MaybePromise<T>(value: T): T | Promise<T> | PromiseLike<T>;
async function doSomething(): Promise<[number, number]> {
const result = await Promise.all([
MaybePromise(100),
MaybePromise(200)
]);
// Error!
//
// [number | Promise<100>, number | Promise<200>]
//
// is not assignable to type
//
// [number, number]
return result;
}
Now Promise.all
 leverages certain features with Awaited
 to give much better inference results, and the above example works.
For more information, you can read about this change on GitHub.
Supporting lib
 from node_modules
To ensure that TypeScript and JavaScript support works well out of the box, TypeScript bundles a series of declaration files (.d.ts
 files). These declaration files represent the available APIs in the JavaScript language, and the standard browser DOM APIs. While there are some reasonable defaults based on your target
, you can pick and choose which declaration files your program uses by configuring the lib
 setting in the tsconfig.json
.
There are two occasional downsides to including these declaration files with TypeScript though:
- When you upgrade TypeScript, you’re also forced to handle changes to TypeScript’s built-in declaration files, and this can be a challenge when the DOM APIs change as frequently as they do.
- It is hard to customize these files to match your needs with the needs of your project’s dependencies (e.g. if your dependencies declare that they use the DOM APIs, you might also be forced into using the DOM APIs).
TypeScript 4.5 introduces a way to override a specific built-in lib
 in a manner similar to how @types/
 support works. When deciding which lib
 files TypeScript should include, it will first look for a scoped @typescript/lib-*
 package in node_modules
. For example, when including dom
 as an option in lib
, TypeScript will use the types in node_modules/@typescript/lib-dom
 if available.
You can then use your package manager to install a specific package to take over for a given lib
 For example, today TypeScript publishes versions of the DOM APIs on @types/web
. If you wanted to lock your project to a specific version of the DOM APIs, you could add this to your package.json
:
{
"dependencies": {
"@typescript/lib-dom": "npm:@types/web"
}
}
Then from 4.5 onwards, you can update TypeScript and your dependency manager’s lockfile will ensure that it uses the exact same version of the DOM types. That means you get to update your types on your own terms.
We’d like to give a shout-out to saschanaz who has been extremely helpful and patient as we’ve been building out and experimenting with this feature.
For more information, you can see the implementation of this change.
Template String Types as Discriminants
TypeScript 4.5 now can narrow values that have template string types, and also recognizes template string types as discriminants.
As an example, the following used to fail, but now successfully type-checks in TypeScript 4.5.
export interface Success {
type: `${string}Success`;
body: string;
}
export interface Error {
type: `${string}Error`;
message: string;
}
export function handler(r: Success | Error) {
if (r.type === "HttpSuccess") {
// 'r' has type 'Success'
let token = r.body;
}
}
For more information, see the change that enables this feature.
--module es2022
Thanks to Kagami S. Rosylight, TypeScript now supports a new module
setting: es2022
. The main feature in --module es2022
is top-level await
, meaning you can use await
outside of async
functions. This was already supported in --module esnext
(and now --module nodenext
), but es2022
is the first stable target for this feature.
You can read up more on this change here.
Tail-Recursion Elimination on Conditional Types
TypeScript often needs to gracefully fail when it detects possibly infinite recursion, or any type expansions that can take a long time and affect your editor experience. As a result, TypeScript has heuristics to make sure it doesn’t go off the rails when trying to pick apart an infinitely-deep type, or working with types that generate a lot of intermediate results.
type InfiniteBox<T> = { item: InfiniteBox<T> }
type Unpack<T> = T extends { item: infer U } ? Unpack<U> : T;
// error: Type instantiation is excessively deep and possibly infinite.
type Test = Unpack<InfiniteBox<number>>
The above example is intentionally simple and useless, but there are plenty of types that are actually useful, and unfortunately trigger our heuristics. As an example, the following TrimLeft
 type removes spaces from the beginning of a string-like type. If given a string type that has a space at the beginning, it immediately feeds the remainder of the string back into TrimLeft
.
type TrimLeft<T extends string> =
T extends ` ${infer Rest}` ? TrimLeft<Rest> : T;
// Test = "hello" | "world"
type Test = TrimLeft<" hello" | " world">;
This type can be useful, but if a string has 50 leading spaces, you’ll get an error.
type TrimLeft<T extends string> =
T extends ` ${infer Rest}` ? TrimLeft<Rest> : T;
// error: Type instantiation is excessively deep and possibly infinite.
type Test = TrimLeft<" oops">;
That’s unfortunate, because these kinds of types tend to be extremely useful in modeling operations on strings – for example, parsers for URL routers. To make matters worse, a more useful type typically creates more type instantiations, and in turn has even more limitations on input length.
But there’s a saving grace:Â TrimLeft
 is written in a way that is tail-recursive in one branch. When it calls itself again, it immediately returns the result and doesn’t do anything with it. Because these types don’t need to create any intermediate results, they can be implemented more quickly and in a way that avoids triggering many of type recursion heuristics that are built into TypeScript.
That’s why TypeScript 4.5 performs some tail-recursion elimination on conditional types. As long as one branch of a conditional type is simply another conditional type, TypeScript can avoid intermediate instantiations. There are still heuristics to ensure that these types don’t go off the rails, but they are much more generous.
Keep in mind, the following type won’t be optimized, since it uses the result of a conditional type by adding it to a union.
type GetChars<S> =
S extends `${infer Char}${infer Rest}` ? Char | GetChars<Rest> : never;
If you would like to make it tail-recursive, you can introduce a helper that takes an “accumulator” type parameter, just like with tail-recursive functions.
type GetChars<S> = GetCharsHelper<S, never>;
type GetCharsHelper<S, Acc> =
S extends `${infer Char}${infer Rest}` ? GetCharsHelper<Rest, Char | Acc> : Acc;
You can read up more on the implementation here.
Disabling Import Elision
There are some cases where TypeScript can’t detect that you’re using an import. For example, take the following code:
import { Animal } from "./animal.js";
eval("console.log(new Animal().isDangerous())");
By default, TypeScript always removes this import because it appears to be unused. In TypeScript 4.5, you can enable a new flag called --preserveValueImports
 to prevent TypeScript from stripping out any imported values from your JavaScript outputs. Good reasons to use eval
 are few and far between, but something very similar to this happens in Svelte:
<!-- A .svelte File -->
<script>
import { someFunc } from "./some-module.js";
</script>
<button on:click={someFunc}>Click me!</button>
along with in Vue.js, using its <script setup>
 feature:
<!-- A .vue File -->
<script setup>
import { someFunc } from "./some-module.js";
</script>
<button @click="someFunc">Click me!</button>
These frameworks generate some code based on markup outside of their <script>
 tags, but TypeScript only sees code within the <script>
 tags. That means TypeScript will automatically drop the import of someFunc
, and the above code won’t be runnable! With TypeScript 4.5, you can use --preserveValueImports
 to avoid these situations.
Note that this flag has a special requirement when combined with --isolatedModules
: imported types must be marked as type-only because compilers that process single files at a time have no way of knowing whether imports are values that appear unused, or a type that must be removed in order to avoid a runtime crash.
// Which of these is a value that should be preserved? tsc knows, but `ts.transpileModule`,
// ts-loader, esbuild, etc. don't, so `isolatedModules` gives an error.
import { someFunc, BaseType } from "./some-module.js";
// ^^^^^^^^
// Error: 'BaseType' is a type and must be imported using a type-only import
// when 'preserveValueImports' and 'isolatedModules' are both enabled.
That makes another TypeScript 4.5 feature, type
 modifiers on import names, especially important.
For more information, see the pull request here.
type
 Modifiers on Import Names
As mentioned above, --preserveValueImports
 and --isolatedModules
 have special requirements so that there’s no ambiguity for build tools whether it’s safe to drop type imports.
// Which of these is a value that should be preserved? tsc knows, but `ts.transpileModule`,
// ts-loader, esbuild, etc. don't, so `isolatedModules` issues an error.
import { someFunc, BaseType } from "./some-module.js";
// ^^^^^^^^
// Error: 'BaseType' is a type and must be imported using a type-only import
// when 'preserveValueImports' and 'isolatedModules' are both enabled.
When these options are combined, we need a way to signal when an import can be legitimately dropped. TypeScript already has something for this with import type
:
import type { BaseType } from "./some-module.js";
import { someFunc } from "./some-module.js";
export class Thing implements BaseType {
// ...
}
This works, but it would be nice to avoid two import statements for the same module. That’s part of why TypeScript 4.5 allows a type
 modifier on individual named imports, so that you can mix and match as needed.
import { someFunc, type BaseType } from "./some-module.js";
export class Thing implements BaseType {
someMethod() {
someFunc();
}
}
In the above example, BaseType
 is always guaranteed to be erased and someFunc
 will be preserved under --preserveValueImports
, leaving us with the following code:
import { someFunc } from "./some-module.js";
export class Thing {
someMethod() {
someFunc();
}
}
For more information, see the changes on GitHub.
Private Field Presence Checks
TypeScript 4.5 supports an ECMAScript proposal for checking whether an object has a private field on it. You can now write a class with a #private
 field member and see whether another object has the same field by using the in
 operator.
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
equals(other: unknown) {
return other &&
typeof other === "object" &&
#name in other && // <- this is new!
this.#name === other.#name;
}
}
One interesting aspect of this feature is that the check #name in other
 implies that other
 must have been constructed as a Person
, since there’s no other way that field could be present. This is actually one of the key features of the proposal, and it’s why the proposal is named “ergonomic brand checks” – because private fields often act as a “brand” to guard against objects that aren’t instances of their class. As such, TypeScript is able to appropriately narrow the type of other
 on each check, until it ends up with the type Person
.
We’d like to extend a big thanks to our friends at Bloomberg who contributed this pull request: Ashley Claymore, Titian Cernicova-Dragomir, Kubilay Kahveci, and Rob Palmer!
Import Assertions
TypeScript 4.5 supports an ECMAScript proposal for import assertions. This is a syntax used by runtimes to make sure that an import has an expected format.
import obj from "./something.json" assert { type: "json" };
The contents of these assertions are not checked by TypeScript since they’re host-specific, and are simply left alone so that browsers and runtimes can handle them (and possibly error).
// TypeScript is fine with this.
// But your browser? Probably not.
import obj from "./something.json" assert {
type: "fluffy bunny"
};
Dynamic import()
 calls can also use import assertions through a second argument.
const obj = await import("./something.json", {
assert: { type: "json" }
})
The expected type of that second argument is defined by a new type called ImportCallOptions
, and currently only accepts an assert
 property.
We’d like to thank Wenlu Wang for implementing this feature!
Const Assertions and Default Type Arguments in JSDoc
TypeScript 4.5 brings some extra expressivity to our JSDoc support.
One example of this is with const
 assertions. In TypeScript, you can get a more precise and immutable type by writing as const
 after a literal.
// type is { prop: string }
let a = { prop: "hello" };
// type is { readonly prop: "hello" }
let b = { prop: "hello" } as const;
In JavaScript files, you can now use JSDoc type assertions to achieve the same thing.
// type is { prop: string }
let a = { prop: "hello" };
// type is { readonly prop: "hello" }
let b = /** @type {const} */ ({ prop: "hello" });
As a reminder, JSDoc type assertions comments start with /** @type {TheTypeWeWant} */
 and are followed by a parenthesized expression:
/** @type {TheTypeWeWant} */` (someExpression)
TypeScript 4.5 also adds default type arguments to JSDoc, which means the following type
 declaration in TypeScript:
type Foo<T extends string | number = number> = { prop: T };
can be rewritten as the following @typedef
 declaration in JavaScript:
/**
* @template {string | number} [T=number]
* @typedef Foo
* @property prop {T}
*/
// or
/**
* @template {string | number} [T=number]
* @typedef {{ prop: T }} Foo
*/
For more information, see the pull request for const
 assertions along with the changes for type argument defaults.
Faster Load Time with realpathSync.native
TypeScript now leverages the realpathSync.native
 function in Node.js on all operating systems.
Previously this function was only used on Linux, but in TypeScript 4.5, as long as you’re running a recent-enough version of Node.js, the compiler will also use the function on operating systems that are typically case-insensitive, like Windows and MacOS. This change sped up project loading by 5-13% on certain codebases on Windows.
For more information, see the original change here, along with the 4.5-specific changes here.
New Snippet Completions
TypeScript 4.5 brings two new snippet completions – these are completions that add some default text and allow you to possibly tab through bits and pieces of the code that you may want to adjust.
Snippet Completions for Methods in Classes
TypeScript 4.5 now provides snippet completions when overriding or implementing methods in classes.
When implementing a method of an interface, or overriding a method in a subclass, TypeScript completes not just the method name, but also the full signature and braces of the method body. When you finish your completion, your cursor will jump into the body of the method.
You can read up more on the development of this feature here.
Snippet Completions for JSX Attributes
TypeScript 4.5 brings snippet completions for JSX attributes. When writing out an attribute in a JSX tag, TypeScript will already provide suggestions for those attributes; but with snippet completions, they can save you a little bit of extra typing by adding an initializer and putting your cursor in the right place.
TypeScript will typically use the type of an attribute to figure out what kind of initializer to insert, but you can customize this behavior in Visual Studio Code.
Keep in mind, this feature will only work in newer versions of Visual Studio Code, so you might have to use an Insiders build to get this working. For more information, read up on the original pull request
Better Editor Support for Unresolved Types
In some cases, editors will leverage a lightweight “partial” semantic mode – either while the editor is waiting for the full project to load, or in contexts like GitHub’s web-based editor.
In older versions of TypeScript, if the language service couldn’t find a type, it would just print any
.
In the above example, Buffer
 wasn’t found, so TypeScript replaced it with any
 in quick info. In TypeScript 4.5, TypeScript will try its best to preserve what you wrote.
However, if you hover over Buffer
 itself, you’ll get a hint that TypeScript couldn’t find Buffer
.
Altogether, this provides a smoother experience when TypeScript doesn’t have the full program available. Keep in mind, you’ll always get an error in regular scenarios to tell you when a type isn’t found.
For more information, see the implementation here.
Experimental Nightly-Only ECMAScript Module Support in Node.js
For the last few years, Node.js has been working to support running ECMAScript modules (ESM). This has been a very difficult feature to support, since the foundation of the Node.js ecosystem is built on a different module system called CommonJS (CJS). Interoperating between the two brings large challenges, with many new features to juggle.
TypeScript 4.5 initially added new settings to support directly running ECMAScript modules in Node.js; however, we believe that the current experience needs more “bake time” before it can be used more broadly. You can see more details of why here.
In turn, this feature is still available for use, but only under nightly releases of TypeScript, and not in TypeScript 4.5.
We are looking to hear what you think so far, so if you’re interested in using TypeScript and running ECMAScript modules under Node.js, read more about this feature in our documentation, try it out, and give us your feedback!
Breaking Changes
lib.d.ts
 Changes
TypeScript 4.5 contains changes to its built-in declaration files which may affect your compilation; however, these changes were fairly minimal, and we expect most code will be unaffected.
Inference Changes from Awaited
Because Awaited
 is now used in lib.d.ts
 and as a result of await
, you may see certain generic types change that might cause incompatibilities. This may cause issues when providing explicit type arguments to functions like Promise.all
, Promise.allSettled
, etc.
Often, you can make a fix by removing type arguments altogether.
- Promise.all<boolean, boolean>(...)
+ Promise.all(...)
More involved cases will require you to replace a list of type arguments with a single type argument of a tuple-like type.
- Promise.all<boolean, boolean>(...)
+ Promise.all<[boolean, boolean]>(...)
However, there will be occasions when a fix will be a little bit more involved, and replacing the types with a tuple of the original type arguments won’t be enough. One example where this occasionally comes up is when an element is possibly a Promise
 or non-Promise
. In those cases, it’s no longer okay to unwrap the underlying element type.
- Promise.all<boolean | undefined, boolean | undefined>(...)
+ Promise.all<[Promise<boolean> | undefined, Promise<boolean> | undefined]>(...)
Template Strings Use .concat()
Template strings in TypeScript previously just used the +
 operator when targeting ES3 or ES5; however, this leads to some divergences between the use of .valueOf()
 and .toString()
 which ends up being less spec-compliant. This is usually not noticeable, but is particularly important when using upcoming standard library additions like Temporal.
TypeScript now uses calls to .concat()
 on strings
. This gives code the same behavior regardless of whether it targets ES3 and ES5, or ES2015 and later. Most code should be unaffected, but you might now see different results on values that define separate valueOf()
 and toString()
 methods.
import moment = require("moment");
// Before: "Moment: Wed Nov 17 2021 16:23:57 GMT-0800"
// After: "Moment: 1637195037348"
console.log(`Moment: ${moment()}`);
More more information, see the original issue.
Compiler Options Checking at the Root of tsconfig.json
It’s an easy mistake to accidentally forget about the compilerOptions
 section in a tsconfig.json
. To help catch this mistake, in TypeScript 4.5, it is an error to add a top-level field which matches any of the available options in compilerOptions
 without having also defined compilerOptions
 in that tsconfig.json
.
Restrictions on Assignability to Conditional Types
TypeScript no longer allows types to be assignable to conditional types that use infer
, or that are distributive. Doing so previously often ended up causing major performance issues. For more information, see the specific change on GitHub.
What’s Next?
We’re already working on TypeScript 4.6! If you’re curious to hear more, you can check out the 4.6 milestone on GitHub until the iteration plan is posted on the TypeScript issue tracker. We currently intend to focus on performance and stability in the next release.
In the meantime, we think TypeScript 4.5 should bring you a lot to love, with many great quality-of-life improvements! We hope that this release makes coding a joy.
Happy Hacking!
– Daniel Rosenwasser and the TypeScript Team
Post contains not just typescript release updates and also typescript language server/Editor updates.
Can’t wait to update my codes for new awaited type. Thanks !
Thanks for your work