Airo Global Software

Think Beyond Future !

Top TypeScript Features Released in 2021

- Posted in TypeScript by

Typescript ended the year with a fantastic 4.1 release. There, the long-awaited Template Literal String and Remapping features were introduced. These opened the door to a lot of new possibilities and patterns.

However, the 4.1 release was only laying the groundwork for Template Literal Types. This feature has matured over the course of the 2021 releases. For example, we can now use Template Literal Types as union discriminates after the 4.5 Release.

There have been four Typescript releases in 2021. They are jam-packed with fantastic features. The core and developer experiences have been vastly improved. In this blog, I will summarise my top 2021 picks. Those are the ones that have the most influence on my daily Typescript.

  • Tuples' Leading Elements

Typescript has long supported the Tuple basic type. It enables us to express a predetermined number of array-type elements.

let arrayOptions: [string, boolean, boolean];

arrayOptions = ['config', true, true]; // works

arrayOptions = [true, 'config', true];

//             ^^^^^  ^^^^^^^^^

// Does not work: incompatible types

function printConfig(data: string) {

 console.log(data);

}

printConfig(arrayOptions[0]);

As part of the tuple, we can define optional elements:

// last 2 elements are optional
let arrayOptions: [string, boolean?, boolean?];
//  A required element cannot follow an optional element
let arrayOptions: [string, boolean?, boolean];

We are implying that our array can have multiple lengths by using the optional modifier. We can even go one step further and define a dynamic length array of the following type:

//  last 2 elements are optional
let arrayOptions: [string, ...boolean[]];
// the below will be all valid
arrayOptions = ['foo', true];
arrayOptions = ['foo', true, true];
arrayOptions = ['foo', true, true, false];

Tuples become more powerful in this new TypeScript. We could previously use the spread operator, but we couldn't specify the last element types.

However, prior to the release of 4.2, the following would be incorrect:

//  An optional element cannot follow a rest element.
let arrayOptions: [string, ...boolean[], number?];

Prior to 4.2, the rest operator had to be the tuple's final element. That is no longer required in the 4.2 release. We can add as many trailing elements as we want without being limited by that constraint. We cannot, however, add an optional element after a spread operator.

//  Prior to 4.2, Error: rest element must be last in a tuple type
let arrayOptions: [string, ...boolean[], number];
// works from 4.2
let arrayOptions: [string, ...boolean[], number];
//  Error: An optional element cannot follow a rest element
let arrayOptions: [string, ...boolean[], number?];

Let’s see more details:

let arrayOptions: [string, ...boolean[], number];
arrayOptions = ['config', 12]; // works
  • Mistakes on Always-Truthful Promise Checks

TypeScript will throw an error when asserting against a promise starting with the 4.3 release, as part of the strictNullChecks configuration.

function fooMethod(promise: Promise) {

   if (promise) {

   // ^^^^^^^^^

   // Error: This condition will always return true since this 'Promise'

   // appears to always be defined.

   // Did you forget to use 'await'

       return 'foo';

   }

   return 'bar';

}

Because the if condition will always be true, the compiler requests that we modify the if statement.

In the config compiler options, there is no additional flag to configure this.

  • Const variables preserve type Guards references.

When asserting an if statement, Typescript will now perform some additional work. If the variable is const or read-only, it will keep its type guard, if it has one go through the below code:

function trim(text: string | null | undefined) {
 const isString = typeof text === "string";
 // prior to 4.4, this const doesn't work as a Type Guard
 if (isString) {
   return text.trim();
   //     ^^^^
   //  Prior to 4.4: Object is possibly 'null' or 'undefined'
   //  Works on 4.4 and onwards
 }
 return text;
}

If the isString variable was of let scope, the preceding code would fail. The Type Guard would be ignored, and the code would fail as shown below:

// isString is instead declared as let
let isString = typeof text === "string";
//  this statement won't work as a Type Guard
if (isString) {
...
}
  • Combining several variables

Type Guard aliases are now smarter and can understand multiple variable combinations.

function concatUppercase(a: string | undefined, b: string | undefined) {
 const bothNonEmpty = a && b;
 //  the Type Guard will work for a and b
 if (bothNonEmpty) {
   //  a and b are of type string
   return `${a.toUpperCase()} ${b.toUpperCase()}`
 }
 return undefined;
}

Both Type Guards are stored in the bothNonEmpty const variable. Within the if statement, a and b are both of the string types. It works transitively When combining variables with Type Guards, those will still be propagated. That means you'll be able to combine as many Type Guards as you want without losing any typing information.

function trim(text: string | null | undefined) {
 const isString = typeof text === "string";
 // prior to 4.4, this const doesn't work as a Type Guard
 if (isString) {
   return text.trim();
   //     ^^^^
   //  Prior to 4.4: Object is possibly 'null' or 'undefined'
   // Works on 4.4 and onwards
 }
 return text;
}

In the preceding example, we can see that both the NonEmpty property and the Type Guard information are retained.

In conclusion, the Control Flow Analysis has been greatly enhanced. The best part is that it will work right away in Typescript 4.4.

  • Specific Optional Property Types

When working with Typescript, there is a recurring debate: should a property be made optional or undefined? It all comes down to personal preference.

So, what's the issue? The Typescript compiler treats both equally. As a result, there is some inconsistency in the code and some friction.

Let’s see an example:

interface User {
 nickName: string;
 email?: string;
// is considered equivalent to
interface User {
 nickName: string;
 email: string | undefined;
}

To put a stop to this inconsistency, Typescript now has a flag called —exactOptionalPropertyTypes. When enabled, it will generate an error if you attempt to treat an optional value as nullable and vice versa. Consider the following code with —exactOptionalPropertyTypes set to true:

interface User {
 nickName: string;
 email?: string;
}
//  Error: Type 'undefined' is not assignable to type 'string'
const user1: User = {
 nickName: 'dioxmio',
 email: undefined
}
//  Works fine, email is optional
const user2: User = {
 nickName: 'max',
}

The code above would be fine if the —exactOptionalPropertyTypes option was not enabled.

To avoid any unintended consequences, it is disabled by default. It is up to us to decide whether or not it is a feature worth having.

  • Symbol Index Signatures and Template Literal Strings

In index signatures, Typescript 4.4 now supports symbol, union, and template literal strings. Unions are allowed as long as they are made up of a string, number, or symbol.

An example using the symbol:

interface Log {
 // symbols are now supported and key type
 [x: symbol]: string;
}
const warn = Symbol('warn');
const error = Symbol('error');
const debug = Symbol('debug');
const log: Log = {};
log[warn] = 'A warning has occurred in line X';

Code using

literal template string:
interface Transaction {
 // template literal string
 [x: `amex-${string}`]: string;
}
const log: Transaction = {};
log['amex-123456'] = '$120';

We can reduce a lot of boilerplate by being able to use unions. We can express our interfaces and types more clearly.

// unions, template literal string and symbols are now supported
interface Foo {
 [x: string | number | symbol | `${string}-id`]: string;
}
// the code above is Equivalent to
interface Foo {
 [x: string]: string;
 [x: number]: string;
 [x: symbol]: string;
 [x: `${string}-id`]: string;
}

The index signatures aren't perfect yet. They have constraints. They continue to lack support for Generic Types and Template Literal Types:

type dice = 1 | 2 | 3 | 4 | 5 | 6;
interface RecordItem {
 //  generics are not supported
 [x: K]: V;
 //  template literal types are not supported
 [x: `${dice}x${dice}`]: string;
}

Nonetheless, this is a fantastic feature addition that will allow us to create more powerful interfaces with fewer lines of code.

  • The Desired Kind

Prior to 4.5, we had to use the infer functionality, as shown below, to determine the return type of a Promise:

type Unwrap = T extends PromiseLike ? U: T;
const resultPromise = Promise.resolve(true);
//  resultUnwrapType is boolean
type resultUnwrapType = Unwrap;

A new type Awaited is included in the 4.5 release. We don't need a custom mapped type like the one described by Unwarp above.

Syntax:

type Result = Awaited;

Use case examples:

/ type is a string
type basic = Awaited>;
//  type is string
type recursive = Awaited>>;
//  type is boolean
type nonThenObj = Awaited;
// type is string | Date
type unions = Awaited>>;
type FakePromise = { then: () => string };
//  type is never
type fake = Awaited;
  • Type-Only Import Parameters

This instructs the TypeScript compiler that this import only contains TypeScript types. What difference does it make? When converting the code to JavaScript, the TSC can safely stripe that import.

//  importing the FC type from React
import type { FC } from 'react';
import { useEffect } from 'react';

As you can see above, the issue is that if you want to be specific about your import types, you must sometimes import statements. You can continue to do the following:

import { FC, useEffect } from 'react';

However, you are sacrificing some readability. You can mix them together starting with version 4.5.

import { type FC, useEffect } from 'react';

This clarifies the code without adding any extra boilerplate.

Conclusion

Typescript has grown in popularity over the years. We can anticipate Typescript becoming the default language for JavaScript-based projects in the near future. In 2021, Typescript has greatly improved. Its core has become smarter, allowing us to rely more on inference. As a result, it is less intrusive and easier to transition from JavaScript code.

The year 2022 is also looking exciting. There are some cool elements on the horizon. If you have any doubt about the best TypeScript features. Don’t hesitate to contact us. Airo Global Softwarewill be your digital partner.

E-mail id: [email protected]

enter image description here

Author - Johnson Augustine
Chief Technical Director and Programmer
Founder: Airo Global Software Inc
LinkedIn Profile: www.linkedin.com/in/johnsontaugustine/