DEV Community

Toluwanimi Isaiah
Toluwanimi Isaiah

Posted on

Let's discuss Generics in TypeScript

This article will discuss Generics, their syntax, their importance and use cases.

Prerequisites

To succesfully follow and understand what will be explained, you need to have the following installed:

  • NodeJS
  • TypeScript
  • Your IDE of choice

Now, what are Generics?

Generics are a way to define reusable functions, classes and interfaces that can work with a variety of types instead of a single type. Generics basically enable us to keep them generic by making them work according to the passed type(s). Take the following function that returns whatever is passed:

function returnArg(arg: string): string {
  return arg;
}
let a = returnArg("Cat");
console.log(a); // "Cat"
Enter fullscreen mode Exit fullscreen mode

We can see that it will not be possible to pass a number to this function, and that is because of the explicit annotation that specifies that it will only accept a string. If we wanted it to work with numbers, one thing we can do is define another function that accepts a number:

function returnArg(arg: number): number {
  return arg;
}
let b = returnArg(2);
console.log(b); // 2
Enter fullscreen mode Exit fullscreen mode

This will work fine, but it results in code repetition which is time consuming and isn't intuitive. An alternative is to set the type of the argument to any:

function returnArg(arg: any): any {
  return arg;
}
let a = returnArg({ name: "Cindy" });
Enter fullscreen mode Exit fullscreen mode

Using any will have the same effect as generics. However, it will cause our program to lose the information about what type was passed and what type will be returned. This means that it is not type-safe and defeats the purpose of type checking.

That's where generics come in. With this approach, we can make the above function work with data of any type without rewriting it. Let's create a generic version of the returnArg function:

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

let a = returnArg(2); // 2
let b = returnArg("a"); // 'a'
let c = returnArg(["cat", "mouse"]); // ["cat". "mouse"]
let d = returnArg({ name: "Tolu", age: 10 }); // { name: "Tolu", age: 10}
Enter fullscreen mode Exit fullscreen mode

The above function uses what is called a type variable which is a special kind of variable that works on types only and not values. It is usually captured within angle brackets. Whatever data type is supplied at the time of the function call is captured in the type variable T to be used later. This allows our function to work on a variety of types while maintaining type safety and preventing code repetition.

As we can see in the variables above, returnArg will accept any data type that is passed into it, without losing any vital information about what type is passed.

Using Generics with multiple parameters of different types

If there are multiple parameters in a function, you can represent each of them within a type variable list like this:

function returnArg<A, B, C>(
  arg1: A,
  arg2: B,
  arg3: C
): { arg1: A; arg2: B; arg3: C } {
  return { arg1, arg2, arg3 };
}

const a = returnArg(1, "Tolu", true);
console.log(a); // { "arg1": 1, "arg2": "Tolu", "arg3": true }
Enter fullscreen mode Exit fullscreen mode

The type variables A, B and C are generic types for whatever arguments will be passed.

Generic interfaces

Not only functions can be generic. Interfaces and classes can too. Here's how a generic interface is written:

interface A {
  a: <T>;
  b: <U>;
  c: <V>;
}
Enter fullscreen mode Exit fullscreen mode

To write a generic interface, we can pass generic types to an interface definition just like functions. It's members will reference the passed types, as seen above.

If for example, we want to represent a person with a generic object, we can create a generic interface for it like this:

interface Person<A, B, C, D> {
  name: <A>;
  age: <B>;
  likesToEat: <C>;
  callName: <D>
}

const person: Person<string, number, boolean, () => void> = {
  name: 'Tolu',
  age: 1,
  likesToEat: true,
  callName: function () {
    alert(this.name)
  }
}
Enter fullscreen mode Exit fullscreen mode

At the point of use of the interface, we are to supply the specific types that we want the type variables to represent as shown in the person variable declaration.

Generic classes

class Pair<T, U> {
  first: T;
  second: U;

  constructor(first: T, second: U) {
    this.first = first;
    this.second = second;
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we've defined a Pair class that can hold two values of any type. The type variables T and U in the class declaration indicate that the class is generic, and that the types of the first and second values will be determined when the class is used.

The Pair class has two properties: first, which holds the first value, and second, which holds the second value. We can create instances of the Pair class with any type of data, like this:

const booleanAndString = new Pair<boolean, string>(true, "Alice");
const objectAndNumber = new Pair<{ greeting: string }, number>(
  { greeting: "hello" },
  42
);
Enter fullscreen mode Exit fullscreen mode

In the first line, we create an instance of Pair with a boolean and a string. In the second line, we create another instance of Pair with a number and an object that has a greeting property. This goes to show the flexibility that generics afford us.

Conclusion

Generics are an important feature of TypeScript that allow us to write functions, interfaces and classes that can work with any data type. This creates flexible, reusable, concise and type safe code which contributes to high quality applications that are easier to maintain. It is worth it to learn about generics and how to put them to effective use within your applications.

I hope that you have gotten some value from this article. Kindly leave any questions or additional information in the comment section.

Thanks for reading!

Top comments (0)