The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
TypeScript 4.5 ships with some amazing features out of the box. These include enhanced Awaited type that improves asynchronous programming, supported lib from node_modules for painless upgrades, and performance optimization by using the realpathSync.native function.
Also, in the beta release, TypeScript added ES modules support for Node 12. However, Microsoft believes this feature would need more “bake time.” Consequently, this feature is only available via an --experimental flag in the nightly builds of TypeScript.
In this article, we would look at the new additions to the feature-packed TypeScript 4.5.
Let’s get started in the next section.
Awaited type and Promise improvementsInspired from the challenges experienced when working with JavaScript inbuilt methods like promise.all, this feature introduces a new Awaited type useful for modeling operations like await in async functions, or the .then() method on a Promise.
This feature adds overloads to promise.all, promise.race, promise.allSettled, and promise.any to support the new Awaited type.
This new type of utility adds the following capabilities:
PromiseLike to resolve promise-like “thenables”neverLastly, some different use cases are:
// basic type = string type basic = Awaited<Promise<string>>; // recursive type = number type recursive = Awaited<Promise<Promise<number>>>; // union type = boolean | number type union = Awaited<string | Promise<number>>;
node_modulesTypeScript ships with a series of declaration files — files ending with.d.ts. These files don’t compile to .js; they are only used for type-checking.
These type declaration files represent the standard browser DOM APIs and all the standardized inbuilt APIs, such as the methods and properties of inbuilt types like string or function, that are available in the JavaScript language.
TypeScript names these declaration files with the pattern lib.[something].d.ts, and they enable Typescript to work well with the version of JavaScript our code is running on.
The properties, methods, and functions available to us depend on the version of JavaScript our code is running on (e.g., the startsWith string method is available on ES6 and above_.
The target compiler setting tells us which version of JavaScript our code runs on and enables us to vary which lib files are loaded by changing the target value.
Consider this code:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
}
}
In the code above, the target value is es5. Thus, ES6 features like startsWith and the arrow function cannot be used in our code. To use ES6 features, we can vary which lib files are loaded by changing es5 to es6.
One of the downsides of this approach is that when we upgrade TypeScript, we are forced to handle changes to TypeScript’s inbuilt declaration files. And this can be challenging with things like DOM APIs that change frequently.
TypeScript 4.5 introduces a new way to vary the inbuilt lib. This method works by looking at a scoped @typescript/lib-* package in node_modules:
{
"dependencies": {
"@typescript/lib-dom": "npm:@types/web"
}
}
In the code above, @types/web represents TypeScript’s published versions of the DOM APIs. And by adding the code above to our package.json file, we lock our project to the specific version of the DOM APIs.
Consequently, when TypeScript is updated, our dependency manager’s lockfile will maintain a version of the DOM types.
TypeScript ships with heuristics that enable it to fail gracefully when compiling programs that have infinite recursion or nonterminating types. This is necessary to prevent stack overflows.
In lower versions, the type instantiation depth limit is 50; this means that after 50 iterations, TypeScript considers that program to be a nonterminating type and fails gracefully.
Consider the code below:
type TrimLeft<T extends string> = T extends ` ${infer Rest}` ? TrimLeft<Rest> : T;
type Test = TrimLeft<" fifty spaces">;
In the code above, the TrimLeft type eliminates leading spaces from string type, but if this leading space exceeds 50 (as in the case above), TypeScript throws this error:
// error: Type instantiation is excessively deep and possibly infinite.
In version 4.5 however, the instantiation limit has been increased to 100 so the above code works.
Also, because in some cases the evaluation of recursive conditional types may require more than 100 iterations, TypeScript 4.5 implements tail-recursive evaluation of conditional types.
The result of this is that when a conditional type ends in another instantiation of the same conditional type, the compiler would evaluate the type in a loop that consumes no extra call stack. However, after a thousand iterations, TypeScript would consider the type nonterminating and throw an error.
In lower versions, by default, TypeScript removes imports that it interprets as unused — in cases that TypeScript cannot detect you are using an import. And this can result in unwanted behavior.
An example of this case is seen when working with frameworks such as Svelte or Vue.
Consider the code below:
<!-- A .svelte File -->
<script>
import { someFunc } from "./some-module.js";
</script>
<button on:click={someFunc}>Click me!</button>
<!-- A .vue File -->
<script setup>
import { someFunc } from "./some-module.js";
</script>
<button @click="someFunc">Click me!</button>
In the code above, we have a sample Svelte and Vue file. These frameworks work similarly as they generate code based on markup outside their script tags. On the contrary, TypeScript only sees code within the script tags, consequently it cannot detect the use of the imported someFunc module. So in the case above, TypeScript would drop the someFunc import, resulting in unwanted behavior.
In version 4.5, we can prevent this behavior by using the new --preserveValueImports flag. This flag prevents TypeScript from removing any import in our outputted JavaScript.
type modifiers on import namesWhile --preserveValueImports prevents the TypeScript compiler from removing useful imports, when working with type import, we need a way to tell the TypeScript compiler to remove them. Type imports are imports that only include TypeScript types and are therefore not needed in our JavaScript output.
Consider the code below:
// 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 { FC, useState } from "react";
const App: FC<{ message: string }> = ({ message }) => (<div>{message}</div>);
In the above code, FC is a type import, but from the syntax, there is no way to tell the TypeScript compiler or build tools to drop FC and preserve useState.
In earlier versions of TypeScript, TS removes this ambiguity by marking type import as type-only:
import type { FC } from "react";
import { useState } from "react";
But TypeScript 4.5 gives us a DRY and cleaner approach:
import {type FC, useState} from "react";
This feature enablesTypeScript to recognize and successfully type-check values that have template string types.
Consider the code below:
type Types =
{
type: `${string}_REQUEST`;
}
| {
type: `${string}_SUCCESS`;
response: string;
};
function reducer2(data: Types) {
if(data.type === 'FOO_REQUEST') {
console.log("Fetcing data...")
}
if (data.type === 'FOO_SUCCESS') {
console.log(data.response);
}
}
console.log(reducer2({type: 'FOO_SUCCESS', response: "John Doe"}))
In the code above, TypeScript is unable to narrow the Types down because it is a template string type. So accessing data.response throws an error:
- Property 'response' does not exist on type 'Types'.
- Property 'response' does not exist on type '{ type: `${string}_REQUEST`; }'.
However, in TypeScript 4.5 this issue is fixed, and TypeScript can now successfully narrow values with template string types.
With this feature, TypeScript supports the JS proposal for ergonomic brand checks for private fields. This proposal enables us to check if an object has a private field by using the syntax below:
class Person {
#password = 12345;
static hasPassword(obj: Object) {
if(#password in obj){
// 'obj' is narrowed from 'object' to 'Person'
return obj.#password;
}
return "does not have password";
}
}
In the code above, the hasPassword static method uses the in operator to check if obj has a private field and returns it.
The problem is that earlier versions of TypeScript would return a strong narrowing hint:
- Property '#password' does not exist on type 'Object'.
However, TypeScript 4.5 provides support for this feature.
TypeScript 4.5 adds support for the import assertion JavaScript proposal. This import assertion feature enables us to pass additional information inline, in the module import statement. This is useful in specifying the type of module.
This feature works with both normal import and dynamic import as seen below:
// normal import
import json from "./myModule.json" assert { type: "json" };
// dynamic import
import("myModule1.json", { assert: { type: "json" } });
With the release of ES modules, JavaScript gives us a standard module definition. And as a result, frameworks like Node.js that are built using a different module definition (like CommonJS) need to provide support ES module. This has been difficult to completely accomplish because the foundation of the Node.js ecosystem is built on CommonJS and not ES modules.
With this feature, TypeScript provides support for ES modules when working with Node.js, but this feature is not available directly on TypeScript 4.5. You can use this feature currently only on the nightly builds of TypeScript.
TypeScript 4.5 enhances the developer experience with these code snippets:
With TypeScript 4.5, when implementing or overriding methods in a class, you get snippet autocompletion.
According to the documentation, 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. And when you finish your completion, your cursor will jump into the body of the method.

With this feature, TypeScript improves the developer experience for writing JSX attributes by adding an initializer and smart cursor positioning as seen below:

This is another feature that is aimed at improving the developer experience. With this feature, TypeScript preserves our code even if it does not have the full program available.
In older versions, when TypeScript cannot find a type it replaces it with any:

However, with this feature, TypeScript would preserve our code (Buffer) and give us a warning when we hover over it as seen below:

--module es2022This new module setting enables us to use top-level await in TypeScript. And this means we can use await outside of async functions.
In older versions, the module options could be none, commonjs, amd, system, umd,es6, es2015, esnext.
Setting the --module option to esnext or nodenext enables us to use top-level await, but the es2022 option is the first stable target for this feature.
realpathSync.nativeThis is a performance optimization change that speeds up project loading by 5–13 percent on certain codebases on Windows, according to the documentation.
This is because the TypeScript compiler now uses the realpathSync.native function in Node.js on all operating systems. Formally this was used on Linux, but if you are running a recent Node.js version, this function would now be used.
const assertions and default type arguments in JSDocWith this feature, TypeScript enhances its support for JSDoc.
JSDoc is an API documentation generator for JavaScript, and with the @type tag, we can provide type hints in our code.
With this feature, TypeScript enhances the expressiveness of this tool with type assertion and also adds default type arguments to JSDoc.
Consider the code below:
// type is { readonly name: "John Doe" }
let developer = { name: "John Doe" } as const;
In the code above, we get a cleaner and leaner immutable type by writing as const after a literal — this is const assertion.
In version 4.5, we can achieve the same expressiveness in JavaScript by using JSDoc:
// type is { readonly prop: "hello" }
let developer = /** @type {const} */ ({ name: "John Doe" });
TypeScript 4.5 is packed with features and enhancements of both the language and the developer experience. You can get more information on TS 4.5 here.
I hope after this article you are ready to start working with TypeScript 4.5.
LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.
With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.
Modernize how you understand your web and mobile apps — start monitoring for free.

Vibe coding isn’t just AI-assisted chaos. Here’s how to avoid insecure, unreadable code and turn your “vibes” into real developer productivity.

GitHub SpecKit brings structure to AI-assisted coding with a spec-driven workflow. Learn how to build a consistent, React-based project guided by clear specs and plans.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now
One Reply to "What’s new in TypeScript 4.5"
Great article !
Just a little remark in the “The Awaited type and Promise improvements”, in the example the union type should be union type = string | number, instead of boolean, if I have understood correctly