With each new version, TypeScript continues to evolve and improve, bringing new features and enhancements to the table. The latest release, TypeScript 5, promises to be smaller, simpler, and faster than its predecessors.
From performance enhancements to streamlined syntax, TypeScript 5 is poised to be a game-changer for developers looking to build high-quality web applications with ease. In this article, we will take a closer look at TypeScript 5 and explore:
Let’s dive in!
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 5 brings a range of new features and enhancements to the table. These include the following:
const type parameters--moduleResolution bundlerextends--verbatimModuleSyntaxexport type *--build@overload support in JSDoc@satisfies support in JSDocIn the rest of this section, we’ll explain each feature and enhancement in the list in more detail.
TypeScript 5 introduces significant improvements in its infrastructure compared to previous versions. The migration from namespaces to modules enables a modular infrastructure, supports modern build tooling, and allows for various optimizations.
In addition to the infrastructure changes, TypeScript 5 offers the following enhancements over TypeScript 4.9:
Furthermore, TypeScript enhances uniformity in object types and reduces the data stored within the compiler, resulting in reduced polymorphic operations and balanced memory usage despite the uniform object shapes.
Decorators in TypeScript 5 work differently from decorators in the previous versions.
The previous versions of TypeScript decorators only allowed developers to alter limited properties of the function they’re modifying. In TypeScript 5, decorators allow developers to replace the function they’re modifying completely — in other words, to completely customize the behavior of a method in a class.
TypeScript 5 decorators work by replacing the method you are applying them to while keeping a reference to that method to call whenever it has done its job.
The following is an example of a decorator:
function monitoredMethod(originalMethod: any, _context: any) {
function replacementMethod(this: any, ...args: any[]) {
console.log('Initializing method call...');
console.log('Calling method...');
let result = originalMethod.call(this, ...args);
console.log('Method called successfully!');
return result;
}
return replacementMethod;
}
If you apply the monitoredMethod decorator to a method, the TypeScript compiler replaces that method with replacementMethod. The compiler references the replaced method for replacementMethod to call whenever it needs to.
Consider the example below:
class Robot {
name: string;
constructor (name: string) {
this.name = name;
}
sayHello(name:string) {
console.log(`${this.name} says hello to ${name}`);
}
}
We can apply the monitoredMethod decorator to the Robot.sayHello method, as done below:
class Robot {
name: string;
constructor (name: string) {
this.name = name;
}
@monitoredMethod
sayHello(name:string) {
console.log(`${this.name} says hello to ${name}`);
}
}
When you call the Robot.sayHello method, you’ll get the output below:
Initializing method call... Calling method... myRobot says hello to everyone Method called successfully!
const type parametersThe const type parameter feature allows developers to create const-like inferences for type parameter declarations. In other words, when the compiler is inferring the type of an object from a function parameter, it chooses a more specific type.
Let’s look at an example below:
function printName<T extends readonly string[]>(args: T, index: number) :void {
console.log(args[index]);
}
printName(["John", "Doe", "Smith"], 1);
The type inference for the printName function call is:
function printName<string[]>(args: string[], index: number): void
Now, let’s see what happens when we add the const type parameter to the printName declaration:
> function printName<const T extends readonly string[]>(args: T, index: number) :void {
console.log(args[index]);
}
printName(["John", "Doe", "Smith"], 1);
The type inference for the printName function call becomes more specific:
function printName<readonly ["John", "Doe", "Smith"]>(args: readonly ["John", "Doe", "Smith"], index: number): void
By using const type parameters, developers can avoid the easy-to-forget process of adding as const in certain places to achieve the desired inference.
However, you should note that the const modifier only affects inferences that you make within the call. const identifiers written outside the call won’t see any change in behavior in the function call:
function printName<const T extends readonly string[]>(args: T, index: number) :void {
console.log(args[index]);
}
const arr = ["John", "Doe", "Smith"];
printName(arr, 1); // `arr` becomes type `string[]` when you pass it to the function
Additionally, it’s crucial to use readonly string[] instead of string[] as the constraint for functions that require immutable arrays. Without the readonly modifier, the compiler won’t give the immutable arrays a more specific type inference. For example:
// For example, this: function printName<readonly ["John", "Doe", "Smith"]>(args: readonly ["John", "Doe", "Smith"], index: number): void // Becomes this: function printName<string[]>(args: string[], index: number): void
--moduleResolution bundlerThe --moduleResolution bundler is a module resolution strategy introduced in TypeScript 4.7. It works for bundlers and runtimes that understand TypeScript natively.
--moduleResolution bundler models how bundlers work. It also implements a hybrid lookup strategy. The strategy is a superset of both the ECMAScript module support introduced in TypeScript 4.7 and CommonJS lookup rules in Node.js.
The --moduleResolution bundler is a good fit for developers using modern bundlers. These bundlers include Vite, Esbuild, SWC, Webpack, and Parcel. It also works for other bundlers that implement a hybrid lookup strategy.
This new module resolution strategy is flexible and customizable. It allows users of different bundlers and runtimes to customize TypeScript’s resolution settings. It supports the resolution of package.json exports and imports. Furthermore, you can enable, disable, or customize configuration options.
Note that the --moduleResolution bundler does not support the resolution of require calls. This means that TypeScript forbids the import mod = require ("foo") syntax in its files. However, require calls are not errors in JavaScript files; they only ever return the type any.
extendsTypeScript’s ability to support multiple configurations in extends is a powerful feature. It allows developers to define a hierarchy of configuration files. They can then choose to extend and override the hierarchy.
This feature comes in handy when managing many projects, especially when working on a large project that requires many configuration files.
Let’s have a look at the tsconfig.json file below:
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../lib",
// ...
}
}
extends allows you to copy over fields from the tsconfig.base.json file to your tsconfig.json file.
There are scenarios where developers might want to extend many configuration files, such as when using a TypeScript base configuration file shipped to npm. To achieve this, TypeScript 5.0 allows the extends field to take many entries. For example:
{
"extends": ["config1.json", "config2.json", "config3.json"],
"compilerOptions": {
// ...
}
}
This allows developers to define a hierarchy of configuration files. You can extend and override any field in the hierarchy in the final configuration file.
Adding many entries in extends is like extending the last configuration file. In this case, the last file extends the second to the last, and so on. If any fields conflict, the latter entry wins.
Have a look at this example:
// tsconfig1.json
{
"compilerOptions": {
"strictNullChecks": true
}
}
// tsconfig2.json
{
"compilerOptions": {
"outDir": "./lib"
}
}
// tsconfig.json
{
"extends": ["./tsconfig1.json", "./tsconfig2.json"],
"files": ["./index.ts"]
}
In this example, the tsconfig.json file inherits both strictNullChecks and outDir. Now, let’s look at another version of the example:
// tsconfig1.json
{
"compilerOptions": {
"strictNullChecks": true,
"outDir": "./build"
}
}
// tsconfig2.json
{
"compilerOptions": {
"outDir": "./lib"
}
}
// tsconfig.json
{
"extends": ["./tsconfig1.json", "./tsconfig2.json"],
"files": ["./index.ts"]
}
In this example, tsconfig.json inherits both configuration files, but outDir will be set to "./lib" because of the hierarchy in extends.
This allows for greater flexibility when defining configuration files for complex projects, especially in projects that need several configuration files. In this case, developers can define a hierarchy of files, and each file defines only the options it requires.
Supporting multiple configurations eliminates the need to define redundant options across many files. This, in turn, makes the configuration process more streamlined and less prone to errors.
Furthermore, using multiple configurations in extends allows developers to create reusable configuration files. You can share these files across many projects. This allows for more consistency in codebases. It also ensures that developers follow best practices across projects.
Finally, supporting many configurations enhances the flexibility and maintainability of TypeScript projects. It encourages developers to define a hierarchy of configurations, which in turn allows greater flexibility in defining the configuration files for complex projects.
Up until version 5, TypeScript categorized enums into two groups: numeric enums and literal — or union — enums.
Numeric enums have several important features, one of which is their ability to automatically assign numeric values to each member. You can also assign them to numeric constants or computed values.
Literal enums give each enum member its own unique type and value. The data type of the enum itself is a “union” of all its member types:
enum Color { Red, Blue, Green, Yellow }
type Colors = Color
type Colors2 = Color.Red | Color.Blue | Color.Green | Color.Yellow
// In this case Color, Colors, and Colors2 are the same type
This is good, but it means that TypeScript determines the value and type of the member at compile time. That means you can only assign string and numeric literals to a member, not computed values. This is because the data type of computed values is usually determined at runtime.
TypeScript 5 introduces a hybrid enum type that has the best of both numeric enums and literal enums. This enum is still called a union type, but it works in a different way from the union type before TypeScript 5:
function getColor(color: string) {
return 4
}
enum Colors {
Red, Green,
Yellow = "yellow",
Orange = getColor("orange") // computed values must be a 'number' datatype
}
// As you can see, numeric and literal enum style works in the Color enum
The major difference between this hybrid and the union enums in previous versions is its flexibility. The example above shows that this enum allows you to compute values in the enum field. In comparison, the union enums from previous TypeScript versions throw errors when you try this.
Previous TypeScript compilers show the error below when you run the code block:
index.ts:8:14 - error TS2553: Computed values are not permitted in an enum with string valued members.
8 Orange = getColor("orange") // computed values must be a 'number' datatype
~~~~~~~~~~~~~~~~~~
Resolution customization flags allow developers to change TypeScript’s package resolution in their projects.
Resolving package imports has always been an issue in TypeScript. TypeScript compiles to JavaScript, which uses a different mechanism for importation and exportation. The compiler needs to resolve the package names to fit JavaScript’s mechanism.
TypeScript 5 introduces five new flags for changing the compiler resolution process:
--allowImportingTsExtensions--resolvePackageJsonExports--resolvePackageJsonImports--allowArbitraryExtensions--customConditionsLet’s look at them each in more detail.
--allowImportingTsExtensionsApplying this flag to tsc allows TypeScript code to import other TypeScript code. This means you can now add a TypeScript extension — i.e., ts, tsx, and mts — to the import name:
// originally you can't add the `.ts` extension to your imports
import app from "./app.ts"; // It'll throw an error
// But with the --allowImportingTsExtension, you can
This flag only works when --noEmit and --emitDeclarationOnly are enabled.
--resolvePackageJsonExportsThis flag prompts tsc to consult the exports field in your package.json file:
// package.json
{
"exports": {
".": "./index.js"
}
}
exports declares entry points for your project. Node checks this field while importing a script in a node_modules package.
exports can be used as an alternative to the main field. In fact, it is a better alternative because it provides more features, like conditional exports and subpath exports.
Note that --resolvePackageJsonExports defaults to true under the node16, nodenext, and bundler options for --moduleResolution.
--resolvePackageJsonImportsThis flag enables the imports field to work for your TypeScript code. The imports field is also an element of the package.json file. It allows developers to create mappings in subpath imports:
import app from './app.js'; // Let's look at this example
/**
* Imagine we fixed the `import` field below in our package.json:
* +--------------------------------+
* | ... |
* | "import": { |
* | "#app": { |
* | "node": "app-node.js", |
* | "default": "app.js" |
* | } |
* | }, |
* | ... |
* +--------------------------------+
*/
// With the `import` field set in our package.json, we can now do this:
import app from "#app";
// The `import` will import the required file based on your environment
// In node, the `import` will import `app-node.js`
// If no condition is set, it'll import `app.js` because it's the default
Like --resolvePackageJsonExports, --resolvePackageJsonImports also defaults to true under the node16, nodenext, and bundler options for --moduleResolution.
--allowArbitraryExtensionsUsually, TypeScript allows developers to import JavaScript files into their code with type definitions. Type definitions are files that end with the .d.ts extension.
Type definitions used to be limited to just JavaScript files. But with TypeScript 5, adding the --allowArbitraryExtensions flag to tsc will allow you to extend type definitions to other types of files.
For example, you can create type definitions for a CSS file and store them in a file with the format {file name}.{extension}.d.ts, or the more recommended {file name}.d.{extension}.ts.
Here’s an example CSS file:
/* app.css */
.send-button {
background-color: green;
}
Here’s the associated app.d.css.ts file:
// app.d.css.ts
declare const styles: {
sendButton: string;
};
export default styles;
Here’s a resulting TypeScript file that imports the CSS file:
// App.tsx import styles from "./app.css"; styles.sendButton;
Whenever the file name is arbitrary, the TypeScript compiler will look for a type definition for that file.
--customConditionsThis flag allows you to define custom conditions that you can apply to conditional exports or imports.
Let’s say you have three files like those indicated below:
// func-to-call.ts
import functionToCall from "#func-to-call";
functionToCall();
// func-to-call-default.ts
export default function func() {
console.log("This is normal");
}
// func-to-call-my-condition.ts
export default function func () {
console.log("This is from the custom condition");
}
We can add a custom condition that makes sure that #func-to-call in the import references the func-to-call-my-condition.ts file. To add the custom condition, add it to the customCondition field in the tsconfig.json file:
{
"compilerOptions": {
"moduleResolution": "nodenext",
"customConditions": ["my-condition"]
}
}
Likewise, we can fit the condition into either the imports or exports in the package.json file:
// imports
{
"imports": {
"#func-to-call": {
"my-condition": "./func-to-call-my-condition.js",
"default": "./func-to-call.js"
}
}
}
// exports
{
"exports": {
".": {
"my-condition": "./func-to-call-my-condition.js",
"default": "./func-to-call.js"
}
}
}
--verbatimModuleSyntaxBefore TypeScript 5 introduced this flag, import elision issues were difficult to handle. Import elision is one of the strategies that TypeScript uses to handle imports.
Let’s take a look at the example below:
// car.ts
type Car = {
name: string,
age: number
};
export default Car;
// index.ts
import Car from "./car";
function car(c: Car) {
// ...
}
The ES JavaScript output of this will be:
// car.js
export {};
// index.js
function car(c) {
// ...
}
export {};
You’ll notice that the import statements are gone. This is the result of import elision. Removing imports that the compiler thinks aren’t necessary improves the runtime of the JavaScript output.
There are questions that can be raised about this behavior. For example, what if the module has an intended side effect and only exports a type? TypeScript wouldn’t be able to tell if the module has an intended side effect and would remove the import statement entirely.
Or, what if the developer imports a class to use as a data type? TypeScript would apply import elision to the import statement.
Another question might be, what if only one of the imports in an import statement is a type, and others are different? Import elision would only remove the imports that are types.
--verbatimModuleSyntax solves issues like the above that can arise with import elisions. This flag gives you an easy way to control elisions that may happen in your code.
To get the desired behavior with the --verbatimModuleSyntax flag, simply use the type modifier. The type modifier lets you mark which import is a type. This helps the flag handle imports the way you want it to. Let’s have a look at this example:
// index.ts
import {Fruit} from "./fruits";
function eatFruit(f: Fruit) {
console.log(`eating ${f.name}...`);
}
// fruits.ts
class Fruit {
name: string;
constructor(name: string) {
this.name = name;
}
}
// intended side effect
console.log('Initializing fruits...');
export {
Fruit
};
We’re only using the Fruit class as a data type, but we want the runtime to execute the module’s side effect.
If we compile this code, the compiler removes the import statement. Without the import statement, the runtime doesn’t run the module. If the runtime doesn’t run the module, it won’t display any side effects.
We want the compiler to handle the module imports, but we also want the runtime to run the module. We can achieve both by applying the --verbatimModuleSyntax to the compiler and applying the type modifier to the Fruit import in the import statement:
import {type Fruit} from "./module-elision-type.js";
function eatFruit(f: Fruit) {
console.log(`eating ${f.name}...`);
}
The JavaScript output of this code will be:
import {} from "./module-elision-type.js";
function eatFruit(f) {
console.log(`eating ${f.name}...`);
}
The runtime executes the module because tsc doesn’t erase the import statement. Since the runtime can run the module, it now displays the intended side effect during runtime.
There are other ways to use the type modifier:
import type { A } from "a"; // Erased entirely.
import { b, type c, type d } from "bcd"; // Rewritten to 'import { b } from "bcd";'
import { type xyz } from "xyz"; // Rewritten to 'import {} from "xyz";'
export type *In TypeScript version 3.8, type-only imports were introduced, but their syntax was not initially supported in exports.
However, with the release of TypeScript 5, you now have the ability to include export * from "module" or export * as namespace from "module" in your code to export these types. This allows for more flexible and comprehensive module exports in TypeScript.
Take a look at this example:
// food/module.ts
export class Fruits {
// ...
}
// food/index.ts
export type * as food from "./module";
// main.ts
import { food } from "./food";
function checkFruit(f: food.Fruits) {
// ...
}
--buildTypeScript now allows you to pass these flags in tsc --build:
--declaration --emitDeclarationOnly --declarationMap --sourceMap --inlineSourceMap They allow you to configure what tsc generates alongside your JavaScript build. These generated files may include source maps, declaration maps, and declaration files.
You may not need these files in development, but you will need them in production. They allow your project to be easily integrated with other projects.
@overload support in JSDocTypeScript provides the ability to define function overloads. This allows you to specify different sets of arguments for a function.
Consider the following scenario:
function printValue(value: string | number, maximumFractionDigits?: number) {
if (typeof value === "number") {
const formatter = Intl.NumberFormat("en-US", {
maximumFractionDigits,
});
value = formatter.format(value);
}
console.log(value);
}
Let’s say we want this function to behave like this:
function printValue(str: string): void; function printValue(num: number, maxFractionDigits?: number): void;
Although it is not possible to achieve the exact behavior, we can use the @overload annotation in the JSDoc of the function. By doing so, the compiler and IDE highlighting can understand the intended usage of the function:
// @ts-check
/**
* @overload
* @param {string} value
* @return {void}
*/
/**
* @overload
* @param {number} value
* @param {number} [maximumFractionDigits]
* @return {void}
*/
/**
* @param {string | number} value
* @param {number} [maximumFractionDigits]
*/
function printValue(value, maximumFractionDigits): void {
if (typeof value === "number") {
const formatter = Intl.NumberFormat("en-US", {
maximumFractionDigits,
});
value = formatter.format(value);
}
console.log(value);
}
@satisfies support in JSDocThis annotation allows you to use a feature like the satisfies operator for JSDoc. satisfies is a keyword that you use on objects to verify if they’re compatible with an interface:
interface Config {
files?: string | string[];
compilerOptions: {
outDir?: string;
};
}
let config = {
files: ["main.ts", "index.ts"],
compilerOptions: {
outDir: "./build"
}
} satisfies Config;
Now have a look at the @satisfies implementation:
// @ts-check
/**
* @typedef CompilerOptions
* @prop {string} [outDir]
*/
/**
* @typedef Config
* @prop {string | string[]} [files]
* @prop {CompilerOptions} [compilerOptions]
*/
/** @satisfies {Config} */
let config2 = {
files: ["main.ts", "index.ts"],
compilerOptions: {
outDir: "./build"
}
};
TypeScript 4.9 introduced the satisfies keyword, and now TypeScript 5 has introduced the @satisfies annotation. Both options are functional in your TypeScript code, but only the @satisfies annotation remains visible even in your JavaScript code.
TypeScript 5.0 introduced several breaking changes and deprecations. Let’s explore each of them in more detail.
TypeScript 5.0 requires a minimum Node.js version of 12.20 and targets ECMAScript 2018.
In TypeScript 5.0, applying a relational operator between a string and a number will result in an error:
function func(ns: number | string) { return ns > 4; }
In the above example, TypeScript prevents implicit string-to-number coercion for the variable ns. This change necessitates explicit type coercion when using the operator. To explicitly coerce ns, prefix it with the + symbol:
function func(ns: number | string) { return +ns > 4; }
Starting from TypeScript 5.0, several flags will no longer be supported. The following flags are affected:
--target es3 --out--noImplicitUseStrict --keyofStringsOnly --suppressExcessPropertyErrors --suppressImplicitAnyIndexErrors --noStrictGenericChecks --charset --importsNotUsedAsValues --preserveValueImports Although these flags can still be used in TypeScript 5.0, they will generate a warning from the compiler. It’s crucial to note that in TypeScript 5.5 and beyond, these flags will be completely unsupported, and attempting to use them will result in an error.
As of this writing, the only flag that appears to have a substitute is --out, which can be replaced with --outFile.
TypeScript 5 introduces significant advancements in web development, providing a host of new features and enhancements that make it more compact, straightforward, and faster than ever before.
With improved performance and streamlined syntax, TypeScript 5 is poised to revolutionize the way developers create high-quality web applications effortlessly.
As a superset of JavaScript, TypeScript continues to evolve and enhance, empowering developers with a robust tool for constructing intricate web applications that are dependable, maintainable, and efficient.
With the advent of TypeScript 5, developers can elevate their web development skills to new heights, unlocking fresh possibilities and pushing the boundaries of what can be achieved on the web.
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.
Would you be interested in joining LogRocket's developer community?
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