Ayooluwa Isaiah I'm a Software Developer from Nigeria with a keen interest in Web Technologies, Security and Performance. I'm currently working on my own products and teaching programming via my website freshman.tech

TypeScript 4.1: New features and improvements

6 min read 1905

typescript 4.1

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.

How to upgrade to TypeScript 4.1

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:

Template literal types

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:

We made a custom demo for .
No really. Click here to check it out.

typescript 4.1 template literal types
typescript 4.1 template literal types

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:

  • The first case matches if T is string or any. In that case, nothing can be inferred from the parameters.
  • The second case matches the parameter at the start of the path and recursively calls itself with a shorter section of the string.
  • The third case matches parameters at the end of the path.
  • In the fourth case, an empty object is returned.

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.

Check indexed access

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.

Key remapping in mapped types

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

Recursive conditional types

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.

JSX factories in React

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.

Notable mentions

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);
  });
}

Conclusion

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!

: Full visibility into your web apps

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

.
Ayooluwa Isaiah I'm a Software Developer from Nigeria with a keen interest in Web Technologies, Security and Performance. I'm currently working on my own products and teaching programming via my website freshman.tech

Leave a Reply