Ten minutes to understand generics in TypeScript

Keywords: Javascript Front-end TypeScript

Please indicate the source of Reprint: the official website of grape City, which provides professional development tools, solutions and services for developers to empower developers.
Original source: https://blog.bitsrc.io/understanding-generics-in-typescript-1c041dc37569

What will you learn in this article

This article introduces the concept and usage of Generics in TypeScript, why it is important, and its usage scenarios. We will introduce its syntax, types and how to build parameters with some clear examples. You can follow the practice in your integrated development environment.

preparation

To learn from this article, you need to prepare the following things on your computer:

Install Node.js: you can run the command line to check whether the Node is installed.

node -v

Install Node Package Manager: usually, when installing a Node, it will install the required version of NPM along with it.
Install TypeScript: if you have installed the Node Package Manager, you can use the following command to install TypeScript in the local global environment.

npm install -g typescript

Integrated development environment: This article will use Visual Studio Code developed by Microsoft team. You can download it here. Enter the download directory and install as prompted. Remember to select the "Add open with code" option so that you can easily open VS Code from anywhere on this machine.
This article is written for TypeScript developers at all levels, including but not just beginners. Here are the steps to set up the working environment to take care of the novices of TypeScript and Visual Studio Code.

What is a generic in TypeScript

In TypeScript, generics are a tool for creating reusable code components. This component can not only be used by one type, but can be reused by multiple types. Similar to the role of parameters, generics is a very reliable means to enhance the capabilities of classes, types and interfaces. In this way, we developers can easily apply those reusable code components to various inputs. However, don't mistake generics in TypeScript for any type -- you'll see the difference later.

Languages like C# and Java, in their toolkits, generics are one of the main means to create reusable code components. That is, it is used to create a code component suitable for multiple types. This allows users to use the generic component with their own classes.

Configure TypeScript in VS Code

Create a new folder on your computer and open it with VS Code (if you follow from scratch, you have installed it).

In VS Code, create an app.ts file. My TypeScript code will be put here.

Copy the following logged code into the editor:

console.log("hello TypeScript");

Press F5 and you will see a launch.json file like this:

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "TypeScript",
      "program": "${workspaceFolder}\\app.ts",
      "outFiles": [
        "${workspaceFolder}/**/*.js"
      ]
    }
  ]
}

The value of the name field inside was originally Launch Program, but I changed it to TypeScript. You can change it to other values.

Click the Terminal Tab, select Run Tasks, select a Task Runner: "TypeScript Watch Mode", and then a tasks.json file will pop up. Change it to the following as follows:

{
 // See https://go.microsoft.com/fwlink/?LinkId=733558
 // for the documentation about the tasks.json format
 "version": "2.0.0",
 "tasks": [
 {
  "label": "echo",
  "type": "shell",
  "command": "tsc",
  "args": ["-w", "-p","."],
  "problemMatcher": [
   "$tsc-watch"
   ],
  "isBackground": true
  }
 ]
}

In the directory where app.ts is located, create another file, tsconfig.json. Copy the following code:

{
  "compilerOptions": {
    "sourceMap": true
  }
}

In this way, the Task Runner can compile TypeScript into JavaScript, monitor file changes, and compile in real time.

Click the terminal tab again, select Run Build Task, and then select tsc: watch - tsconfig.json. You can see the information on the terminal:

[21:41:31] Starting compilation in watch mode...

You can compile TypeScript files using the debugging function of VS Code.

With the development environment set up, you can start dealing with issues related to the generic concept of TypeScript.

Find the problem

any type is not recommended in TypeScript for several reasons, as you can see in this article. One reason is the lack of complete information during debugging. A good reason to choose VS Code as a development tool is the intelligent perception based on this information.

If you have a class that stores a collection. There are ways to add things to the collection, and there are ways to get things in the collection through the index. like this:

class Collection {
  private _things: string[];
  constructor() {
    this._things = [];
  }
  add(something: string) {
    this._things.push(something);
  }
  get(index: number): string {
    return this._things[index];
  }
}

You can quickly recognize that this collection is displayed and defined as a collection of string type. Obviously, you can't use number in it. If you want to handle numbers, you can create a collection that accepts numbers instead of strings. It is a good choice, but it has one big disadvantage - code duplication. Code duplication will eventually lead to more time to write and debug code and reduce the efficiency of memory.

Another option is to use any type instead of string type to define the class just now, as follows:

class Collection {
  private _things: any[];
  constructor() {
    this._things = [];
  }
  add(something: any) {
    this._things.push(something);
  }
  get(index: number): any {
    return this._things[index];
  }
}

At this point, the collection supports any type you give. If you create logic like this to build this collection:

let Stringss = new Collection();
Stringss.add("hello");
Stringss.add("world");

This adds the strings "hello" and "world" to the collection. You can type an attribute like length to return the length of any collection element.

console.log(Stringss.get(0).length);

The string "hello" has five characters. Run TypeScript code and you can see it in debug mode.

Please note that when you hover over the length attribute, the IntelliSense of VS Code does not provide any information because it does not know the exact type you choose to use. When you modify one of the added elements to other types, such as number, as follows, this situation that cannot be perceived by intelligence will be more obvious:

let Strings = new Collection();
Strings.add(001);
Strings.add("world");
console.log(Strings.get(0).length);

You hit an undefined result and there is still no useful information. If you go further and decide to print the substring of string - it will report a runtime error, but it does not indicate any specific content. More importantly, the compiler does not give any compile time errors with type mismatches.

console.log(Stringss.get(0).substr(0,1));

This is just a consequence of using the any type to define the collection.

Understanding the central idea

The problem caused by using any type just now can be solved by using generics in TypeScript. Its central idea is type safety. Using generics, you can specify instances of classes, types, and interfaces in a way that the compiler can understand and judge. As in other strongly typed languages, this method can find your type errors at compile time, so as to ensure type safety.

The syntax of generics is like this:

function identity<T>(arg: T): T {
  return arg;
}

You can use generics in previously created collections, enclosed in angle brackets.

class Collection<T> {
  private _things: T[];
  constructor() {
    this._things = [];
  }
  add(something: T): void {
    this._things.push(something);
  }
  get(index: number): T {
    return this._things[index];
  }
}
let Stringss = new Collection<String>();
Stringss.add(001);
Stringss.add("world");
console.log(Stringss.get(0).substr(0, 1));

If you copy the new logic with angle brackets into the code editor, you will immediately notice the wavy line under "001". This is because TypeScript can now infer that 001 is not a string from the specified generic type. Where T occurs, the string type can be used, which realizes type safety. In essence, the output of this collection can be of any type, but you indicate that it should be of string type, so the compiler infers that it is of string type. The generic declaration used here is at the class level. It can also be defined at other levels, such as static method level and instance method level, as you will see later.

Use Generics

You can include multiple type parameters in the generic declaration. They only need to be separated by commas, like this:

class Collection<T, K> {
  private _things: K[];
  constructor() {
    this._things = [];
  }
  add(something: K): void {
    this._things.push(something);
  }
  get(index: number): T {
    console.log(index);
  }
}

When declared, type parameters can also be explicitly used in functions, such as:

class Collection {
  private _things: any[];
  constructor() {
    this._things = [];
  }
  add<A>(something: A): void {
    this._things.push(something);
  }
  get<B>(index: number): B {
    return this._things[index];
  }
}

Therefore, when you want to create a new collection, the generics declared at the method level will now be indicated at the method call level, such as:

let Stringss = new Collection();
Stringss.add<string>("hello");
Stringss.add("world");

You can also notice that when the mouse hovers, VS Code IntelliSense can infer that the second add function call is still of type string.

Generic declarations also apply to static methods:

static add<A>(something: A): void {
  _things.push(something);
}

Although generic types can be used when initializing static methods, they cannot be used when initializing static properties.

Generic constraints

Now that you have a good understanding of generics, it's time to mention the core shortcomings of generics and their practical solutions. Using generics, the types of many attributes can be inferred by TypeScript. However, in some places where TypeScript cannot make accurate inference, it will not make any assumptions. For type safety, you need to define these requirements or constraints as interfaces and inherit them in generic initialization.

If you have such a very simple function:

function printName<T>(arg: T) {
  console.log(arg.length);
  return arg;
}
printName(3);

Because TypeScript cannot infer what type the arg parameter is and cannot prove that all types have a length attribute, it cannot be assumed that it is a string (with a length attribute). Therefore, you will see a wavy line under the length attribute. As mentioned earlier, you need to create an interface so that the initialization of generic types can inherit it so that the compiler will no longer alarm.

interface NameArgs {
  length: number;
}

You can inherit it in a generic declaration:

function printName<T extends NameArgs>(arg: T) {
  console.log(arg.length);
  return arg;
}

This tells TypeScript that any type with the length attribute can be used. After defining it, the function call statement must also be changed because it no longer applies to all types. So it should look like this:

printName({length: 1, value: 3});

This is a very basic example. But by understanding it, you can see how useful it is to set generic constraints when using generics.

Why generics

Behrooz, an active member of the Stack Overflow community, answered this question well in the follow-up content. The main reason for using generics in TypeScript is to make types, classes, or interfaces act as parameters. It helps us reuse the same code for different types of input, because the type itself can be used as a parameter.

Some of the benefits of generics are:

Defines the relationship between input and output parameter types. such as

function test<T>(input: T[]): T {
  //...
}

Allows you to ensure that the input and output use the same type, even though the input is an array.

You can use more powerful type checking at compile time. In the appeal example, the compiler lets you know that the array method can be used for input, and no other method can.
You can remove unnecessary casts. For example, if you have a list of constants:

Array<Item> a = [];

Variable array, you can access all members of Item type by smart perception.

Other resources
Official documents

conclusion

You've seen an overview of the generic concept and seen various examples to help reveal the ideas behind it. At first, the concept of generics can be confusing, and I suggest reading this article again and reviewing the additional resources provided in this article to help you understand it better. Generics is a great concept that can help us better control input and output in JavaScript. Please code happily!

Posted by sstoveld on Sun, 05 Dec 2021 21:44:38 -0800