Let's discuss Generics in TypeScript
Published: 2023-03-14 09:40:00

This article will discuss [Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html), 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:

```typescript
function returnArg(arg: string): string {
  return arg;
}
let a = returnArg("Cat");
console.log(a); // "Cat"
```

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:

```typescript
function returnArg(arg: number): number {
  return arg;
}
let b = returnArg(2);
console.log(b); // 2
```

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`:

```typescript
function returnArg(arg: any): any {
  return arg;
}
let a = returnArg({ name: "Cindy" });
```

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:

```typescript
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}
```

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:

```typescript
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 }
```

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:

```typescript
interface A {
  a: <T>;
  b: <U>;
  c: <V>;
}
```

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:

```typescript
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)
  }
}
```

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

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

  constructor(first: T, second: U) {
    this.first = first;
    this.second = second;
  }
}
```

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:

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

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!