What’s new in TypeScript 4.5
Tags: typescript, webdev, programming
Published: 2021-12-20 20:37:51

**Written by [Lawrence Eagles](https://blog.logrocket.com/author/lawrenceoputa/) ✏️**

## Introduction

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.

## New Features

### The `Awaited` type and `Promise` improvements

Inspired 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:

1.  1.  Recursive unwrap
    2.  Does not require `PromiseLike` to resolve promise-like “thenables”
    3.  Non-promise “thenables” resolve to `never`

Lastly, some different use cases are:

```typescript
// 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>>;
```

### Supporting lib from `node_modules`

TypeScript 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:

```ts
{
  "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`:

```ts{
 "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.

### Tail-recursion elimination on conditional 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:

```ts
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:

```ts
// 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.

### Disabling import elision

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:

```ts
<!-- 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 names

While `--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:

```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 { 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:

```ts
import type { FC } from "react";
import { useState } from "react";
```

But TypeScript 4.5 gives us a DRY and cleaner approach:

```ts
import {type FC, useState} from "react";
```

### Template string types as discriminants

This feature enablesTypeScript to recognize and successfully type-check values that have template string types.

Consider the code below:

```ts
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:

```ts
- 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.

### Private field presence checks

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:

```ts
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:

```ts
  - Property '#password' does not exist on type 'Object'.
```

However, TypeScript 4.5 provides support for this feature.

### Import assertions

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:

```ts
// normal import
import json from "./myModule.json" assert { type: "json" }; 

// dynamic import
import("myModule1.json", { assert: { type: "json" } });
```

### Experimental nightly-only ECMAScript module support in Node.js

With the release of [ES modules](https://blog.logrocket.com/es-modules-in-node-today/), 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](https://www.typescriptlang.org/docs/handbook/nightly-builds.html) of TypeScript.

### New snippet completions

TypeScript 4.5 enhances the developer experience with these code snippets:

#### Snippet completions for methods in classes

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.

![Snippet completions in TS 4.5](https://blog.logrocket.com/wp-content/uploads/2021/12/snippet-completions-ts-4-5.png) Source: [TS DevBlog](https://devblogs.microsoft.com/typescript/announcing-typescript-4-5/#subclass-method-snippets)

#### Snippet completions for JSX attributes

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

![Snippet Completions for TS4.5 and JSX](https://blog.logrocket.com/wp-content/uploads/2021/12/snippet-completions-ts-jsx.gif) Source: [TS DevBlog](https://devblogs.microsoft.com/typescript/announcing-typescript-4-5/#jsx-attribute-snippets)

### Better editor support for unresolved types

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`:

![TS Replace Types with Any](https://blog.logrocket.com/wp-content/uploads/2021/12/ts-replace-type-any.png) Source: [TS DevBlog](https://devblogs.microsoft.com/typescript/announcing-typescript-4-5/#display-unresolved-types)

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

![TS Buffer](https://blog.logrocket.com/wp-content/uploads/2021/12/ts-buffer.png) Source: [TS DevBlog](https://devblogs.microsoft.com/typescript/announcing-typescript-4-5/#display-unresolved-types)

### `--module es2022`

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

### Faster load times with `realpathSync.native`

This 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 JSDoc

With this feature, TypeScript enhances its support for [JSDoc](https://jsdoc.app).

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:

```ts
// 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:

```ts
// type is { readonly prop: "hello" }
let developer = /** @type {const} */ ({ name: "John Doe" }); 
```

## Conclusion

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](https://devblogs.microsoft.com/typescript/announcing-typescript-4-5/).

I hope after this article you are ready to start working with TypeScript 4.5.


---

## Writing a lot of TypeScript? [Watch the recording](https://blog.logrocket.com/typescript-meetup-recap/?utm_source=social&utm_medium=organic&utm_campaign=21Q3_WB_TS-Tech-Meetup0930#utm_source=social&utm_medium=organic&utm_campaign=21Q3_WB_TS-Tech-Meetup0930) of our recent TypeScript meetup to learn about writing more readable code.

[![TypeScript meetup header](https://blog.logrocket.com/wp-content/uploads/2021/07/typescript-4-4-more-readable-code.png)](https://blog.logrocket.com/typescript-meetup-recap/?utm_source=social&utm_medium=organic&utm_campaign=21Q3_WB_TS-Tech-Meetup0930#utm_source=social&utm_medium=organic&utm_campaign=21Q3_WB_TS-Tech-Meetup0930)