TypeScript 4.1 became the latest stable release of the language in late November. This new version of the language builds upon the features included in version 4.0 with improvements including new checking flags, productivity tracking, and bug fixes to help developers express types in a more precise manner.
In this article, we’ll take a look at how some of the features included in this release will affect the way we write our code going forward.
You can install or upgrade to the latest version of TypeScript through npm:
$ npm install -D typescript
If you want to skip installation, you can try out the new features in TypeScript through the TypeScript Playground. The TypeScript Playground supports the latest version so you can test any code included in this article without leaving your web browser.
Let’s go ahead and examine some of the most interesting features offered in TypeScript 4.1:
TypeScript has two ways to represent strings in the language: a string
type, which covers all strings, and a string literal type, which matches only matches a specific string. You can use union types in combination with string literal types to be even more precise regarding allowed string inputs. Take a look at the following:
let a: string = 'Hello' a = 'World' // works let b: 'Hello' = 'Hello' b = 'World' // Type '"World"' is not assignable to type '"Hello"'. declare function selectOperatingSystem(os: 'windows' | 'linux' | 'macos'): void selectOperatingSystem('windows'); // works selectOperatingSystem('linux'); // works selectOperatingSystem('android'); // Argument of type '"android"' is not assignable to parameter of type '"windows" | "linux" | "macos"'.
With TypeScript 4.1, it’s now possible to use concatenate strings in types with the help of template literal string types. It uses the same syntax as template literals in JavaScript, but in the type position. A template literal string type produces a new string literal type by concatenating the contents of the template string:
type Name = 'John'; type Greeting = `Hello ${Name}`; // Equal to: type Greeting = 'Hello John'
The power of template literal types becomes apparent when you use union types in substitution positions. The type produced will be a union of all the possible combinations of the string literals:
type Names = 'John' | 'Jake' | 'Sarah'; type Greeting = `Hello ${Name}`; // Equal to: type Greeting = 'Hello John' | 'Hello Jake' | 'Hello Sarah'
You can also have multiple union types in the template literal, which will then produce a union with every possible combination of the types in the resulting type:
type Suit = "Hearts" | "Diamonds" | "Clubs" | "Spades"; type Rank = "Ace" | "Two" | "Three" | "Four" | "Five" | "Six" | "Seven" | "Eight" | "Nine" | "Ten" | "Jack" | "Queen" | "King"; type Card = `${Rank} of ${Suit}`; declare function printCard(c: Card): void printCard('Ace of Spades'); // works printCard('Ace of Diamond'); // error
This becomes apparent when you hover over type
in your editor:
A terrific use case that this feature enables is the ability to parse route parameters in Node.js web frameworks like Express where it is common to use placeholders to represent the dynamic parts of a route (such as an ID
).
app.get("/users/:userId", (req, res) => { const { userId } = req.params; res.send(`User ID: ${userId}`); });
The req.params
object is a ParamsDictonary
which has the following signature:
interface ParamsDictionary { [key: string]: string; }
All TypeScript knows is that the object will have string keys with string values. If you try to access a parameter that is not specified in the route path, then the compiler will allow it anyway because it doesn’t know that only userId
can exist.
We can use template literal types to provide a more accurate type for the req.params
object by inferring from substitution positions:
type ExtractRouteParams<T extends string> = string extends T ? Record<string, string> : T extends `${infer Start}:${infer Param}/${infer Rest}` ? {[k in Param | keyof ExtractRouteParams<Rest>]: string} : T extends `${infer Start}:${infer Param}` ? {[k in Param]: string} : {}; type params = ExtractRouteParams<'/users/:userId/posts/postId'>; // Same as: type params = { userId: string; postId: string; }
Let’s break it down a bit:
T
is string
or any
. In that case, nothing can be inferred from the parameters.This pattern makes it easy to extract a valid type of {userId: string}
out of /users/:userId
as demonstrated in the example. A collection of other (and sometimes over the top) use cases can be found in this GitHub repository.
Index signatures are a way to specify to the TypeScript compiler that it is possible to access object properties that are not explicitly defined on the type.
type Person = { name: string; email: string; // index signature [key: string]: string; } function logPerson(p: Person): void { console.log(p.name) // string console.log(p.email) // string console.log(p.nationality.toLowerCase()) // OK, nationality is a string } const person: Person = { name: "Jack", email: "[email protected]", }; logPerson(person);
Any property accessed under Person
except those that are explicitly listed will have the type string
. The problem with this feature is that any property access that resolves to an index signature is potentially undefined
, but this is not reflected in the type. In the block above, for example, p.nationality
is potentially undefined, but the type information is string
.
TypeScript 4.1 provides a way to fix this undesirable behavior with the introduction of the noUncheckedIndexAccess
flag. When enabled, any property or index access that resolves to an index signature will now resolve to a union of the specified type and undefined
. Check out the example below:
function logPerson(p: Person): void { console.log(p.name) // string console.log(p.email) // string console.log(p.nationality.toLowerCase()) // Error when noUncheckedIndexedAccess is enabled // ~~~~~~~~~~~~~ Object is possibly 'undefined'. }
To fix the error, you can now use a type guard to narrow the type or the non-null assertion operator (!
):
if (p.nationality) { console.log(p.nationality.toLowerCase()) } console.log(p.nationality!.toLowerCase())
Another effect of enabling the noUncheckedIndexedAccess
flag is that indexing into an array will also yield a union of the type of the array element and undefined
, even if the index has been bounds checked.
let arr = [1, 2, 3, 4]; for (let i = 0; i < arr.length; i++) { const n: number = arr[i]; // Error // Type 'number | undefined' is not assignable to type 'number'. console.log(n); }
The same error occurs even when using an array method such as forEach()
:
let arr = [1, 2, 3, 4]; arr.forEach((e, i) => { const n: number = arr[i]; // Error // Type 'number | undefined' is not assignable to type 'number'. console.log(n); });
Of course you can use the element directly in the above example without accessing it through its index, but this use case illustrates the effects of enabling this flag in your codebase. You might get a lot of errors if you perform indexed access on an array without checking for the existence of the element, but, as before, you can use a type guard or non-null operator to fix the error.
Note: this behavior is not enabled as part of the strict
level options, so you need to explicitly enable it by setting the noUncheckedIndexedAccess
option to true
under compilerOptions
in your configuration file.
Mapped types are an invaluable tool in TypeScript. They are a helpful tool for deriving types from other types in order to limit duplication whilst ensuring that the types stay in sync.
Here’s an example that creates a new object type based on another object type, and consequently makes all properties on the new type nullable:
type Person = { name: string; age: number; } // `Nullable<T>` is the same as T but each property is nullable type Nullable<T> = { [K in keyof T]: T[K] | null; }; type NullablePerson = Nullable<Person> // same as // type NullablePerson = { // name: string | null; // age: number | null; // };
In TypeScript 4.1, you can now remap the object keys with a new as
clause. This provides flexibility by allowing the creation of new property names from existing ones. Notice how the template literal types are used in the example below to facilitate the object key remapping:
type Company = { name: string; employees: number; ceo: string; } type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; }; type ComapanyAccessors = Getters<Company> // same as // type ComapanyAccessors = { // getName: () => string; // getEmployees: () => number; // getCeo: () => string // };
As you can see, keys have been capitalized through the Capitalize
helper. Similar helpers include: Uncapitalize<T>
, Uppercase<T>
, and Lowercase<T>
.
When conditional types were first introduced in version 2.8, they were restricted to be non-recursive as a safeguard against infinite recursion which was not well supported at the time. With the release of TypeScript 4.1, it is now possible to express conditional types that immediately reference themselves within their branches. Look at the example below to see how Awaited
deeply unwraps Promise:
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T; type P1 = Awaited<Promise<string>>; // string type P2 = Awaited<Promise<Promise<string>>>; // string type P3 = Awaited<Promise<string | Promise<Promise<number> | undefined>>>; // string | number | undefined
Note that the TypeScript compiler needs more time for type checking recursive types. If the internal recursion depth limit is reached, a compile-time error will occur. Despite the power of these recursive types, Microsoft cautions that they should be used responsibly and sparingly to avoid failures when they are employed on complex inputs.
Support for React 17’s upcoming jsx
and jsxs
factory functions have been included in this release through the react-jsx
and react-jsxdev
options for the jsx
compiler flag. The react-jsx
extension is intended for production builds, while the latter, react-jsxdev
, is for development.
{ "compilerOptions": { "module": "esnext", "target": "es2015", "strict": true, "jsx": "react-jsx" }, "include": [ "./**/*" ] }
TypeScript typings and syntaxes can be difficult to keep up with, especially when used with React. Learn more here.
Setting the checkJs
compiler option also enables allowJs
in TypeScript 4.1. Previously, both options had to be set explicitly which seemed redundant.
Another improvement worth noting is that the argument to resolve()
in Promise constructors are no longer optional except if Promise<void>
is set as the generic type argument.
So, for example, when you write code such as the following …
function resolveAfter2Seconds() { return new Promise(resolve => { setTimeout(() => { resolve(); }, 2000); }); }
… You may get an error like the following:
Expected 1 arguments, but got 0. Did you forget to include 'void' in your type argument to 'Promise'?
The fix is to add the void
generic parameter as the error suggests:
function resolveAfter2Seconds() { return new Promise<void>(resolve => { setTimeout(() => { resolve(); }, 2000); }); }
It is encouraging to see the continued evolution of TypeScript towards a better developer experience and simpler, more intuitive APIs. This article details the major features and improvements in the 4.1 release only, and not historical TypeScript features. If you’re curious about the full range of changes, be sure to check the official release notes.
Thanks for reading, and happy coding!
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
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 nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.