Was this page helpful?

TypeScript 4.5

Supportinglib fromnode_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 yourtarget, you can pick and choose which declaration files your program uses by configuring thelib setting in thetsconfig.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-inlib in a manner similar to how@types/ support works.When deciding whichlib files TypeScript should include, it will first look for a scoped@typescript/lib-* package innode_modules.For example, when includingdom as an option inlib, TypeScript will use the types innode_modules/@typescript/lib-dom if available.

You can then use your package manager to install a specific package to take over for a givenlibFor 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 yourpackage.json:

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 tosaschanaz who has been extremely helpful and patient as we’ve been building out and experimenting with this feature.

For more information, you cansee the implementation of this change.

TheAwaited Type andPromise Improvements

TypeScript 4.5 introduces a new utility type called theAwaited type.This type is meant to model operations likeawait inasync functions, or the.then() method onPromises - specifically, the way that they recursively unwrapPromises.

ts
// A = string
typeA =Awaited<Promise<string>>;
// B = number
typeB =Awaited<Promise<Promise<number>>>;
// C = boolean | number
typeC =Awaited<boolean |Promise<number>>;

TheAwaited type can be helpful for modeling existing APIs, including JavaScript built-ins likePromise.all,Promise.race, etc.In fact, some of the problems around inference withPromise.all served as motivations forAwaited.Here’s an example that fails in TypeScript 4.4 and earlier.

ts
declarefunctionMaybePromise<T>(value:T):T |Promise<T> |PromiseLike<T>;
asyncfunctiondoSomething():Promise<[number,number]> {
constresult =awaitPromise.all([MaybePromise(100),MaybePromise(200)]);
// Error!
//
// [number | Promise<100>, number | Promise<200>]
//
// is not assignable to type
//
// [number, number]
returnresult;
}

NowPromise.all leverages the combination of certain features withAwaited to give much better inference results, and the above example works.

For more information, youcan read about this change on GitHub.

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.

ts
exportinterfaceSuccess {
type:`${string}Success`;
body:string;
}
 
exportinterfaceError {
type:`${string}Error`;
message:string
}
 
exportfunctionhandler(r:Success |Error) {
if (r.type ==="HttpSuccess") {
consttoken =r.body;
(parameter) r: Success
}
}
Try

For more information,see the change that enables this feature.

module es2022

Thanks toKagami S. Rosylight, TypeScript now supports a newmodule setting:es2022.The main feature inmodule es2022 is top-levelawait, meaning you can useawait outside ofasync functions.This was already supported in--module esnext (and now--module nodenext), butes2022 is the first stable target for this feature.

You canread 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.

ts
typeInfiniteBox<T> = {item:InfiniteBox<T> };
typeUnpack<T> =Textends {item:inferU } ?Unpack<U> :T;
// error: Type instantiation is excessively deep and possibly infinite.
typeTest =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 followingTrimLeft 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 intoTrimLeft.

ts
typeTrimLeft<Textendsstring> =
Textends`${inferRest}` ?TrimLeft<Rest> :T;
// Test = "hello" | "world"
typeTest =TrimLeft<" hello" |" world">;

This type can be useful, but if a string has 50 leading spaces, you’ll get an error.

ts
typeTrimLeft<Textendsstring> =
Textends`${inferRest}` ?TrimLeft<Rest> :T;
// error: Type instantiation is excessively deep and possibly infinite.
typeTest =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 istail-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 typewon’t be optimized, since it uses the result of a conditional type by adding it to a union.

ts
typeGetChars<S> =
Sextends`${inferChar}${inferRest}` ?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.

ts
typeGetChars<S> =GetCharsHelper<S,never>;
typeGetCharsHelper<S,Acc> =
Sextends`${inferChar}${inferRest}` ?GetCharsHelper<Rest,Char |Acc> :Acc;

You can read up more on the implementationhere.

Disabling Import Elision

There are some cases where TypeScript can’t detect that you’re using an import.For example, take the following code:

ts
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 calledpreserveValueImports to prevent TypeScript from stripping out any imported values from your JavaScript outputs.Good reasons to useeval are few and far between, but something very similar to this happens in Svelte:

html
<!-- A .svelte File -->
<script>
import {someFunc }from"./some-module.js";
</script>
<buttonon:click="{someFunc}">Click me!</button>

along with in Vue.js, using its<script setup> feature:

html
<!-- A .vue File -->
<scriptsetup>
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 TypeScriptonly sees code within the<script> tags.That means TypeScript will automatically drop the import ofsomeFunc, and the above code won’t be runnable!With TypeScript 4.5, you can usepreserveValueImports to avoid these situations.

Note that this flag has a special requirement when combined with—isolatedModules`: importedtypesmust 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.

ts
// 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 andisolatedModules have special requirements so that there’s no ambiguity for build tools whether it’s safe to drop type imports.

ts
// 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 withimport type:

ts
importtype {BaseType }from"./some-module.js";
import {someFunc }from"./some-module.js";
exportclassThingimplementsBaseType {
// ...
}

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 atype modifier on individual named imports, so that you can mix and match as needed.

ts
import {someFunc,typeBaseType }from"./some-module.js";
exportclassThingimplementsBaseType {
someMethod() {
someFunc();
}
}

In the above example,BaseType is always guaranteed to be erased andsomeFunc will be preserved underpreserveValueImports, leaving us with the following code:

js
import {someFunc }from"./some-module.js";
exportclassThing {
someMethod() {
someFunc();
}
}

For more information, seethe 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 thein operator.

ts
classPerson {
#name:string;
constructor(name:string) {
this.#name =name;
}
equals(other:unknown) {
returnother &&
typeofother ==="object" &&
#nameinother &&// <- this is new!
this.#name ===other.#name;
}
}

One interesting aspect of this feature is that the check#name in other implies thatother must have been constructed as aPerson, 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 ofother on each check, until it ends up with the typePerson.

We’d like to extend a big thanks to our friends at Bloombergwho contributed this pull request:Ashley Claymore,Titian Cernicova-Dragomir,Kubilay Kahveci, andRob Palmer!

Import Assertions

TypeScript 4.5 supports an ECMAScript proposal forimport assertions.This is a syntax used by runtimes to make sure that an import has an expected format.

ts
importobjfrom"./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).

ts
// TypeScript is fine with this.
// But your browser? Probably not.
importobjfrom"./something.json"assert {
type: "fluffybunny"
};

Dynamicimport() calls can also use import assertions through a second argument.

ts
constobj =awaitimport("./something.json", {
assert: {type:"json" },
});

The expected type of that second argument is defined by a new type calledImportCallOptions, and currently only accepts anassert property.

We’d like to thankWenlu Wang forimplementing 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 withconst assertions. In TypeScript, you can get a more precise and immutable type by writingas const after a literal.

ts
// type is { prop: string }
leta = {prop:"hello" };
// type is { readonly prop: "hello" }
letb = {prop:"hello" }asconst;

In JavaScript files, you can now use JSDoc type assertions to achieve the same thing.

ts
// type is { prop: string }
leta = {prop:"hello" };
// type is { readonly prop: "hello" }
letb =/**@type{const} */ ({prop:"hello" });

As a reminder, JSDoc type assertions comments start with/** @type {TheTypeWeWant} */ and are followed by a parenthesized expression:

js
/**@type{TheTypeWeWant} */` (someExpression)

TypeScript 4.5 also adds default type arguments to JSDoc, which means the followingtype declaration in TypeScript:

ts
typeFoo<Textendsstring |number =number> = {prop:T };

can be rewritten as the following@typedef declaration in #"https://github.com/microsoft/TypeScript/pull/45464">the pull request for const assertions along withthe changes for type argument defaults.

Faster Load Time withrealPathSync.native

TypeScript now leverages a system-native implementation of the Node.jsrealPathSync function on all operating systems.

Previously this function was only used on Linux, but in TypeScript 4.5 it has been adopted to operating systems that are typically case-insensitive, like Windows and MacOS.On certain codebases, this change sped up project loading by 5-13% (depending on the host operating system).

For more information, seethe original change here, along withthe 4.5-specific changes here.

Snippet Completions for JSX Attributes

TypeScript 4.5 bringssnippet 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 remove a little bit of extra typing by adding an initializer and putting your cursor in the right place.

Snippet completions for JSX attributes. For a string property, quotes are automatically added. For a numeric properties, braces are added.

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.

Settings in VS Code for JSX attribute completions

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 likeGitHub’s web-based editor.

In older versions of TypeScript, if the language service couldn’t find a type, it would just printany.

Hovering over a signature where Buffer isn't found, TypeScript replaces it with any.

In the above example,Buffer wasn’t found, so TypeScript replaced it withany inquick info.In TypeScript 4.5, TypeScript will try its best to preserve what you wrote.

Hovering over a signature where Buffer isn't found, it continues to use the name Buffer.

However, if you hover overBuffer itself, you’ll get a hint that TypeScript couldn’t findBuffer.

TypeScript displays type Buffer = /* unresolved */ any;

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.

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 fromAwaited

BecauseAwaited is now used inlib.d.ts and as a result ofawait, you may see certain generic types change that might cause incompatibilities;however, given many intentional design decisions aroundAwaited to avoid breakage, we expect most code will be unaffected.

Compiler Options Checking at the Root oftsconfig.json

It’s an easy mistake to accidentally forget about thecompilerOptions section in atsconfig.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 incompilerOptionswithout having also definedcompilerOptions in thattsconfig.json.

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

Contributors to this page:
OTOrta Therox  (4)
Eescudero89  (1)
RRetsam  (1)
Nnavya9singh  (1)
HGHolger Grosse-Plankermann  (1)
4+

Last updated: Feb 18, 2026