Tutorial

How To Use Generics in TypeScript

TypeScript

Introduction

Following the Don’t-Repeat-Yourself (DRY) principle is important when it comes to writing dynamic and reusable code. Using generics can help you achieve this in your TypeScript code.

With generics, you can write dynamic and reusable generic blocks of code. Furthermore, you can apply generics in TypeScript to classes, interfaces, and functions.

In this article, you will integrate generics into your TypeScript code and apply them to functions and classes. You will also learn how to add constraints to generics in TypeScript by using interfaces.

Prerequisites

To successfully complete this tutorial, you will need the following:

Step 1 — Understanding Generics

Sometimes, you may want to repeat the same block of code for different data types. Here’s an example of the same function being used for two different data types:

// for number type
function fun(args: number): number {
  return args;
}

// for string type
function fun(args: string): string {
  return args;
}

Note that, in this example, the same function is being repeated for number and string types. Generics can help you to write generalized methods instead of writing and repeating the same block of code, as in the above example.

There is a type called any, which you can use to achieve the same effect as generics in your code. Using the any type will allow you to opt-out of type-checking. However, any is not type-safe. This means that using any can give you an exception.

To see this in practice, apply the any type to the previous code example:

function fun(args: any): any {
 return args;
}

Swapping number and string types to the any type makes the function generic. But there’s a catch—using the any type means that the fun function can accept any data. As a result, you are losing type safety as well.

Although using the any type is a way to make your TypeScript code more generic, it may not always be the best option. In the next step, you explore a different option for creating type-safe generics.

Step 2 — Creating Type-Safe Generics

To create type-safe generics, you will need to use Type parameters. Type parameters are defined by T or <T>. They denote the data type of passed parameters to a class, interface, and functions.

Returning to the fun function, use T to make your generic function type-safe:

index.ts
function fun<T>(args:T):T {
  return args;
}

As a result, fun is now a type-safe generic function. To test this type-safe generic function, create a variable named result and set it equal to fun with a string type parameter. The argument will be the Hello World string:

index.ts
let result = fun<string>("Hello World");

Try using the fun function with the number type. Set the argument equal to 200:

index.ts
let result2 = fun<number>(200);

If you would like to see the results of this code, you can include console.log statements to print result and result2 to the console:

index.ts
console.log(result);
console.log(result2);

In the end, your code should look like this:

index.ts
function fun<T>(args:T):T {
  return args;
}

let result = fun<string>("Hello World");
let result2 = fun<number>(200);

console.log(result);
console.log(result2);

Use ts-node to run this TypeScript code in the console:

  • npx ts-node index.ts

The code renders without error. You will see this output:

Output
Hello World 200

You can now create type-safe generics for functions with one parameter. It’s also important to know how to create generics for functions with multiple parameters of many different types.

Step 3 — Using Generics with Parameters of Many Types

If there are many parameters in a function, you can use different letters to denote the types. You don’t have to only use T:

params.ts
function fun<T, U, V>(args1:T, args2: U, args3: V): V {
  return args3;
}

This function takes 3 parameters, args1, args2, and arg3, and returns args3. These parameters are not restricted to a certain type. This is because T, U, and V are used as generic types for the fun function’s parameters.

Create a variable called result3 and assign it to fun. Include the <string, number, boolean> types to fill in the T, U, and V generic types. For the arguments, include a string, a number, and boolean held within parentheses:

params.ts
let result3 = fun<string, number, boolean>('hey', 3, false);

This will return the third argument, false. To see this, you can use a console.log statement:

params.ts
console.log(result3);

Run the ts-node command to see your console.log statement output:

  • npx ts-node params.ts

This will be the output:

Output
false

Now you can create generic types for functions with multiple parameters. Like functions, generics can be used with classes and interfaces as well.

Step 4 — Creating Generic Classes

Like generic functions, classes can be generic too. The type parameter in angle (<>) brackets are used, as with functions. Then the <T> type is used throughout the class for defining methods and properties.

Create a class that takes both number and string inputs and creates an array with those inputs. Use <T> as the generic type parameter:

classes.ts
class customArray<T> {
  private arr: T[] = [];
}

Now, your array that takes in items of different types is in place. Create a method called getItems that returns the customArray array:

classes.ts
getItems (arr: T[]) {
  return this.arr = arr;
}

Create a method called addItem that adds new items to the end of the customArray array:

classes.ts
addItem(item:T) {
  this.arr.push(item);
}

The arr: T[] argument means that the items within the array can be of any type. So customArray can be an array of numbers, booleans, or strings.

Add a method called removeItem that removes specified items from the customArray:

classes.ts
removeItem(item: T) {
  let index = this.arr.indexOf(item);
    if(index > -1)
      this.arr.splice(index, 1);
}

Like the addItem method, removeItem takes a parameter of any type and removes the specified parameter from the customArray array.

Now the generic class customArray is complete. Create an instance of customArray for number and string types.

Declare a variable called numObj set equal to an instance of customArray for number types:

classes.ts
let numObj = new customArray<number>();

Use the addItem method to add the number 10 to numObj:

classes.ts
numObj.addItem(10);

Since customArray is generic, it can also be used to create an array of strings. Create a variable called strObj set equal to an instance of customArray for string types:

classes.ts
let strObj = new customArray<string>();

Use the addItem method to add the string Robin to the strObj array.

classes.ts
strObj.addItem(“Robin”);

To see the results of your code, create a console.log statement for both numObj and strObj:

classes.ts
console.log(numObj);
console.log(strObj);

In the end, your code should look like this:

classes.ts
class customArray<T> {
  private arr: T[] = [];

  getItems(arr: T[]) {
    return this.arr = arr;
  }

  addItem(item:T) {
    this.arr.push(item);
  }

  removeItem(item: T) {
    let index = this.arr.indexOf(item);
      if(index > -1)
        this.arr.splice(index, 1);
  }
}

let numObj = new customArray<number>();
numObj.addItem(10);

let strObj = new customArray<string>();
strObj.addItem(“Robin”);

console.log(numObj);
console.log(strObj);

After running ts-node, this is the output you will receive:

Output
customArray { arr: [ 10 ] } customArray { arr: [ 'Robin' ] }

You used the customArray class for both number and string types. You were able to accomplish this by using generic types. However, using generics does have some constraints. This will be discussed in the next step.

Step 5 — Understanding Generic Constraints

Up until this point, you’ve created functions and classes using generics. But there is a drawback to using generics. To see this drawback in action, write a function called getLength that will return the length of the function’s argument:

constraints.ts
function getLength<T>(args: T) : number {
  return args.length;
}

This function will work as long as the passing type has a length property, but data types that don’t have a length property will throw an exception.

There is a solution to this problem—creating generic constraints. To do this, you will first need to create an interface called funcArgs and define a length property:

constraints.ts
interface funcArgs {
  length: number;
}

Now, change the getLength function and extend it to include the funcArgs interface as a constraint:

constraints.ts
function getLength<T extends funcArgs>(args:T) : number {
  return args.length;
}

You’ve created a generic constraint using an interface. Furthermore, you also extended the getLength function with this interface. It now needs length as a required parameter. Accessing this getLength function with an argument that doesn’t have a length parameter will show an exception message.

To see this in action, declare a variable called result4 and assign it to getLength with 3 as its argument:

constraints.ts
let result4 = getLength(3);

This will return an error since a value for the length parameter is not included:

Output
⨯ Unable to compile TypeScript: index.ts:53:25 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'funcArgs'. 53 let result4 = getLength(3);

To call the getLength function, you will need to include a length argument along with a name argument:

constraints.ts
let result = getLength({ length: 5, name: 'Hello'});

This is the right way of calling our function. This call has a length property, and your function will work well. It will not show any error message.

Conclusion

In this tutorial, you successfully integrated generics into your TypeScript functions and classes. You also included constraints for your generics.

As a next step, you may be interested in learning how to use TypeScript with React. If you would like to learn how to work with TypeScript in VS Code, this How To Work With TypeScript in Visual Studio Code article is a great place to start.

Creative Commons License