Written by Lawrence Eagles ✏️
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. Recursive unwrap
- Does not require
PromiseLike
to resolve promise-like “thenables” - Non-promise “thenables” resolve to
never
- Does not require
Lastly, 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>>;
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:
{
"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.
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:
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.
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:
<!-- 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:
// 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";
Template string types as discriminants
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.
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:
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.
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:
// 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, 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.
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.
Source: TS DevBlog
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:
Source: TS DevBlog
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
:
Source: TS DevBlog
However, with this feature, TypeScript would preserve our code (Buffer
) and give us a warning when we hover over it as seen below:
Source: TS DevBlog
--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.
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" });
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.
I hope after this article you are ready to start working with TypeScript 4.5.
Top comments (0)