Recursive conditional types, React JSX factories, and more new features in the new TypeScript release

One of the biggest things I’ve learned from writing TypeScript for many years is how easy it is to understand — especially for people with a Java background. Still, after hearing the news about TypeScript 4.1, the language’s most recent major update, I was surprised by the new features.

I don’t think I’m an ignorant exception. After using the news as an opportunity to gain insight into how the type system actually works, I wanted to share with you the exciting features and changes in the new release, along with keyword descriptions and lots of magic examples.

If you have a solid foundation in the TypeScript language and are eager to learn advanced features, get started.

New language features

Template literals

Since ES6, we have been able to write strings in backquotes, not just single or double quotes, through the feature of Template Literals:

const message = `text`;
Copy the code

As Flavio Copes notes, template literals provide features that previously were not available in quotation marks:

  • Defining multi-line strings is handy
  • Interpolation of variables and expressions can be done easily
  • You can create DSLS (Domain Specific Languages) with template tags

Template literals have the same syntax as template strings in JavaScript, except that they are used in type definitions:

type Entity = 'Invoice';

type Notification = `${Entity} saved`;
/ / is equivalent to
// type Notification = 'Invoice saved';


type Viewport = 'md' | 'xs';
type Device = 'mobile' | 'desktop';

type Screen = `${Viewport | Device} screen`;
// Equivalent to the following line
// type Screen = 'md screen' | 'xs screen' | 'mobile screen' | 'desktop screen';
Copy the code

When we define a specific literal type, TypeScript produces a new string literal type by concatenating the content.

Key Remapping in key-value pair types

Mapping types can create new object types based on any key. String literals can be used as attribute names in mapping types:

type Actions = { [K in 'showEdit' | 'showCopy' | 'showDelete']? :boolean; };
/ / is equivalent to
typeActions = { showEdit? :boolean, showCopy? :boolean, showDelete? :boolean
};
Copy the code

If you want to create new keys or filter them out, TypeScript 4.1 allows you to remap keys in mapped types using new AS clauses:

type MappedTypeWithNewKeys<T> = {
    [K in keyof T as NewKeyType]: T[K]
}
    
Copy the code

TypeScript Remapping Keys The new as clause lets you leverage features like template literal types to easily create new Property names based on old ones. Keys can be filtered by producing never so that you don’t have to use an extra Omit Helper type in some cases: By using the new AS clause, we can easily create new property names based on old properties using features like template literal types. We can filter keys by printing never so that we don’t have to use additional Omit types in some cases:

type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`] :() = > T[K]
};

interface Person {
    name: string;
    age: number;
    location: string;
}

type LazyPerson = Getters<Person>;
// ^ = type LazyPerson = {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
/ /}

// Remove the 'kind' attribute
type RemoveKindField<T> = {
    [K in keyof T as Exclude<K, "kind">]: T[K]
};

interface Circle {
    kind: "circle";
    radius: number;
}

type KindlessCircle = RemoveKindField<Circle>;
// ^ = type KindlessCircle = {
// radius: number;
/ /}
Copy the code

TypeScript leverages template text types with as clauses (source)

JSX factory functions

JSX stands for JavaScript XML and allows us to write HTML elements in JavaScript and place them in the DOM without any createElement() or appendChild() methods, such as:

const greeting = <h4>Yes I can do it!</h4>;
ReactDOM.render(greeting, document.getElementById('root'));
Copy the code

TypeScript 4.1 supports React 17’s JSX and JSXS factory functions via two new options in compiler option JSX:

  • react-jsx
  • react-jsxdev

“These options are used for production and development compilation, respectively. Often, one option can be extended from another.” – TypeScript release description

Here are two examples of TypeScript configuration documents for production and development:

// ./src/tsconfig.json
{
  "compilerOptions": {
    "module": "esnext"."target": "es2015"."jsx": "react-jsx"."strict": true
  },
  "include": [". / / * * *"]}Copy the code

Development configuration:

// ./src/tsconfig.dev.json
{
  "extends": "./tsconfig.json"."compilerOptions": {
    "jsx": "react-jsxdev"}}Copy the code

TypeScript 4.1 supports type checking in JSX environments like React, as shown below:

Recursive condition type

Another new feature is recursive conditional types, which allow them to refer to themselves in branches, giving them more flexibility with conditional types and making it easier to write recursive type aliases. Here is an example of a deeply nested Promise that uses a column expansion:

type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;

/ / similar ` promise. Then (...). ', but more precise in type
declare function customThen<T.U> (
    p: Promise<T>,
    onFulfilled: (value: Awaited<T>) => U
) :Promise<Awaited<U>>;
Copy the code

It should be noted, however, that TypeScript takes more time to type check recursive types. Microsoft warns that they should be used responsibly and carefully.

Checked Indexed accesses

Index signatures in TypeScript allow arbitrary named attributes to be accessed as in the Options interface below:

interface Options {
  path: string;
  permissions: number;

  // Extra properties are caught by this index signature.
  // Additional attributes will be added by this
  [propName: string] :string | number;
}

function checkOptions(opts: Options) {
  opts.path; // string
  opts.permissions; // number

  // All of these are ok! Because the type is string | number
  opts.yadda.toString();
  opts["foo bar baz"].toString();
  opts[Math.random()].toString();
}
Copy the code

Here, we see is not the attribute of the path and permissions should have string | number type:

TypeScript 4.1 provides a new flag –noUncheckedIndexedAccess, making it possible for each property access (e.g. Opts.path) or index access (e.g. Opts [” blabla “] to be undefined. This means that if we need to access an attribute like opts.path from the previous example, we must check for its presence or use a non-NULL assertion operator (suffix! Characters) :

function checkOptions(opts: Options) {
  opts.path; // string
  opts.permissions; // number

  // The following code is invalid if noUncheckedIndexedAccess is enabled
  opts.yadda.toString();
  opts["foo bar baz"].toString();
  opts[Math.random()].toString();

  // Check if the attribute really exists
  if (opts.yadda) {
    console.log(opts.yadda.toString());
  }

  // Use the non-null assertion operator directlyopts.yadda! .toString(); }Copy the code

The noUncheckedIndexedAccess flag is useful for catching many errors, but can be noisy for a lot of code. This is why the –strict switch does not automatically enable it.

Don’t needbaseUrlThe specified path

Prior to TypeScript 4.1, to be able to use paths in tsconfig.json files, you had to declare the baseUrl parameter. In the new version, baseUrl can be specified without the PATHS option. This solves the problem of poor paths in automatic imports.

{
    "compilerOptions": {
        "baseUrl": "./src"."paths": {
            "@shared": ["@shared/"] // This mapping is relative to "baseUrl"}}}Copy the code

checkJsThe defaultallowJs

If you have a JavaScript project in which you are checking for errors in the.js file using the checkJs option, you should also declare allowJs to allow compiling the JavaScript file. In TypeScript 4.1, checkJs means allowJs by default:

{
  compilerOptions: {
    allowJs: true.checkJs: true}}Copy the code

JSDoc @seeEditor support for tabs

There is now better support for the JSDoc tag @see when using TypeScript in editors, which will improve the usability of TypeScript 4.1:

// @filename: first.ts
export class C {}

// @filename: main.ts
import * as first from "./first";

/ * * *@see first.C
 */
function related() {}
Copy the code

Incompatible change

lib.d.tschange

The environment declarations of structure and DOM make it easy to start writing type-checked JavaScript code.

This file is automatically included in the compilation context of a TypeScript project. You can exclude it by specifying the –noLib compiler command line flag or setting noLib to true in tsconfig.json.

In TypeScript 4.1, since DOM types are automatically generated, lib.d.ts may have a variable set of apis, such as reflect.enumerate, removed from ES2016.

abstractMembers cannot be marked asasync

In another major change, members marked as abstract cannot be re-marked as async. Therefore, to fix your code, you must remove the async keyword:

abstract class MyClass {
  Async must be removed in TypeScript 4.1
  abstract async create(): Promise<string>;
}
Copy the code

any/unknownSpread outward

Before TypeScript 4.1, foo was of type any or unknown for expressions like foo && somethingElse. The type of the entire expression will be somethingElse, which in the following example is {someProp: string} :

declare let foo: unknown;
declare let somethingElse: { someProp: string };
let x = foo && somethingElse;
Copy the code

In TypeScript 4.1, both any and unknown propagate outwards, not to the right. In general, the appropriate solution for this change is to switch from foo && someExpression to!! Foo && someExpression.

Note: the double exclamation mark (!!) Is a convenient way to cast a variable to a Boolean value (true or false).

In the PromiseresolveIs no longer an optional type

The resolve argument in Promise is no longer optional, as in the following code:

new Promise((resolve) = > {
  doSomethingAsync(() = > {
    doSomething();
    resolve();
  });
});
Copy the code

This code returns an error when compiled in TypeScript 4.1:

resolve()
  ~~~~~~~~~
error TS2554: Expected 1 arguments, but got 0.
  An argument for 'value' was not provided.
Copy the code

To solve this problem, resolve must be given at least one value in the Promise. Otherwise, the Promise must be declared with an explicit void generic type argument in cases where resolve() is really called without arguments:

new Promise<void> ((resolve) = > {
  doSomethingAsync(() = > {
    doSomething();
    resolve();
  });
});
Copy the code

Conditional expansion creates optional properties

In JavaScript, the expansion operator {… Files} does not apply to false values, such as files null or undefined.

In the following example using conditional propagation, if file is defined, the file.owner property is propagated. Otherwise, no attributes are propagated to the returned object:

function getOwner(file? : File) {
  return{... file? .owner,defaultUserId: 123}; }Copy the code

Prior to TypeScript 4.1, getOwner returned a union type based on each expanded object:

{ x: number } | { x: number.name: string.age: number.location: string }
Copy the code
  • If I definefile, will have all the attributes from Person (the owner’s type).
  • Otherwise, none of the results will be shown

But as it turns out, that price can end up being prohibitively high and often unhelpful. There are hundreds of expanded objects in a single object, and each expanded object may add hundreds or thousands of attributes. For better performance, in TypeScript 4.1, returned types sometimes use all optional attributes:

{
    x:         number; name? :string; age? :number; location? :string;
}
Copy the code

Parameters that do not match will no longer be associated

In the past, parameters that didn’t correspond to each other were associated in TypeScript by associating them with the any type.

In the following overloaded example (providing multiple types of functionality for the same functionality), the pickCard function returns two different contents based on what the user passes in. If the user passes in an object representing deck, the function selects card. If the user selects card, they will get the card of their choice:

let suits = ["hearts"."spades"."clubs"."diamonds"];

function pickCard(x: { suit: string; card: number }[]) :number;
function pickCard(x: number) :{ suit: string; card: number };
function pickCard(x: any) :any {
  // Check to see if we're working with an object/array
  // if so, they gave us the deck and we'll pick the card
  if (typeof x == "object") {
    let pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;
  }
  // Otherwise just let them pick the card
  else if (typeof x == "number") {
    let pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13}; }}let myDeck = [
  { suit: "diamonds".card: 2 },
  { suit: "spades".card: 10 },
  { suit: "hearts".card: 4},];let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
Copy the code

With TypeScript 4.1, assignment will fail in some cases, and overload resolution will fail in others. The solution is that it is best to use type assertions to avoid errors.

One last thought

TypeScript saves us time by catching errors and providing fixes before we run the code. By taking a closer look at TypeScript, we can better understand how to improve code structure and get solutions to complex problems. Hopefully, this article has helped you explore the type system and make your programming journey more exciting.

TypeScript 4.1 is available via NuGet or NPM:

npm install typescript
Copy the code