Mastering Generics in Typescript
Tags: typescript, generics, functions, interfaces
Published: 2023-07-14 16:37:47

---
title: Mastering Generics in Typescript
published: true
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zm6o5eyk9eg9s6xbzk6f.png
date: 2023-07-14 16:10:40 UTC
tags: typescript, generics, functions, interfaces
canonical_url: https://blog.jamin.sh/mastering-generics-in-typescript
---

Generics are one of the superpowers of TypeScript, and while they might look scary if one has little or no knowledge about them, they can be life-saving when understood and used correctly. Mastering generics in TypeScript can significantly enhance one's ability to write clean, robust, and scalable code. In this article, we will explore the fundamentals of generics, delve into some of its concepts, and provide practical examples to help individuals become better users of generics in TypeScript.

## What are Generics in Typescript?

Generics enable us to create reusable components that work with different data types while maintaining type safety. They provide a way to define placeholders for types that are determined when the code is used. This enables us to write more generic and flexible code.

Consider this example of this function below:

```ts
function echo(value) {
  return value;
}

//Call the function
echo("Hello World")

```

The `echo` function receives a value and returns that value without any modification. At the moment, TypeScript frowns at our function parameter and complains that parameter 'value' implicitly has an 'any' type. meaning that since theres no type explicitly specified, TypeScript has inferred a type of `any` to the parameter of the function. This doesnt help us check the data of the parameter of our function, so how do we check that the right type is passed when our function is called and that our function also returns the correct data?

One way we could do this is by explicitly setting the type of parameter `value` passed and returned.

```ts
function echo(value:string):string {
  return value;
}
echo("Hello World")

```

This example works perfectly as we know the type of data being passed as a parameter and the data our function returns, but there is a tiny bit of an issue. What if we want to make the `echo` function reusable and call it somewhere else with a different data type? Typescript will complain that were passing the wrong data type to the `echo` function.

```ts
function echo(value:string): string {
  return value;
}
//Argument of type 'number' is not assignable to parameter of type 'string'
echo(3.14159)

```

So how do we fix this? How do we create a reusable function that accepts and returns the types we pass to it? Generics!

## Basic Generics

Starting with the basics, using the `echo` function as an example, lets spice it up and update the function with generics to be reusable.

```ts
function echo<T> (value: T): T {
  return value;
}

 //calls the function with no errors from typescript
echo("Hello World")
echo(3.14159);
echo(false);
echo([1, 2, 3, 4])
echo({id: 1, fullName: "John Doe"})

```

In this example, The echo function is a generic function that takes an argument `value` of type `T` and returns the exact value of type `T` without any modification. The generic type parameter `<T>` we passed to the function allows it to work with different types, thus allowing us to reuse the same function with different data.

Generally, you can write a generic type with any alphabet you like, `<X>,` `<Y>`, but `T` is for `type`, and it's just a tradition of naming generics, and there is nothing to prevent you from using other names or alphabets.

## Generics on Type Declarations

Generics dont only work with `functions`; we can also use generics to make types more robust, reusable, and flexible when declared. For example, let's say we are building an e-commerce application and have a Repository interface defining common CRUD operations for working with different entities in our system. Instead of creating separate interfaces for each entity (e.g., `UserRepository`, `ProductRepository`, etc.), we can use generics to create a single, generic `Repository` interface that can handle various entity types.

```ts
interface Repository<T> {
  getById(id: string): T | undefined;
  getAll(): T[];
  create(item: T): void;
  update(item: T): void;
  delete(id: string): void;
}
class UserRepository implements Repository<User> {
  // Implementation specific to User entity
}

class ProductRepository implements Repository<Product> {
  // Implementation specific to Products operation
}

// Usage
const userRepository: Repository<User> = new UserRepository();
const user = userRepository.getById('123');
userRepository.create(newUser);

const productRepository: Repository<Product> = new ProductRepository();
const products = productRepository.getAll();
productRepository.update(updatedProduct);

```

In this example, the `Repository` interface is defined with a generic type parameter `T` representing the entity type. It provides common methods like `getById`, `getAll`, `create`, `update`, and `delete` that can be used with any entity.

By implementing the `Repository` interface with specific entity types like `User` and `Product`, we can create specialized repositories that handle operations particular to those entities. The generic nature of the interface allows for code reuse and flexibility when working with different types of entities.

This approach makes the code more modular, maintainable, and extensible, as we can easily add new entity-specific repositories without duplicating the same set of methods for each entity type.

## Generics in Functions with Type Constraints

As weve seen from our first generics example in this article, we can use generics with functions and combine them with some other features of TypeScript to produce more exciting results. Lets say we want a function called `printLength` that accepts only items with lengths and prints the length of any items we pass on it. Using the `extend` keyword in Typescript, we can create a function that constrains the data passed to the function.

```ts
interface Lengthy {
  length: number;
}

function printLength<T extends Lengthy>(item: T): void {
  console.log(`Length: ${item.length}`);
}

const stringItem = "Hello, World!";
printLength(stringItem); // Output: Length: 13

const arrayItem = [1, 2, 3, 4, 5];
printLength(arrayItem); // Output: Length: 5

const numberItem = 42; // Error: Type 'number' does not have a property 'length'
printLength(numberItem);

```

In this example, The `printLength` function takes a generic type parameter `T` that extends the `Lengthy` interface using the `extends` keyword. This means the type `T` must have the `length` property of the type `number`.

Within the `printLength` function, we can safely access the `length` property of the `item` argument without causing type errors because the generic type `T` is guaranteed to have a `length` property.

Using the `printLength` function with items(`stringItem` and `arrayItem` ) that satisfy the `Lengthy` requirements, we got no errors from TypeScript. However, when we try to pass a number (`numberItem`), which doesn't have a `length` property, TypeScript raises an error.

Lets consider another example that combines generics with some more inbuilt features of TypeScript. The function below accepts an array of objects, finds an object by the `id` key property of the object, and returns the `key` value we specified as a parameter when calling the function.

```ts
interface Person {
  id: number;
  name: string;
  age: number;
}

function getObjectValue<T extends { id: number }, K extends keyof T>
  (arr: T[], key: K, id: number): T[K] | undefined {
  const foundObject = arr.find((obj) => obj.id === id);
  return foundObject ? foundObject[key] : undefined;
}

const people: Person[] = [
  { id: 1, name: "John", age: 25 },
  { id: 2, name: "Jane", age: 30 },
  { id: 3, name: "Bob", age: 40 },
];

const nameValue = getObjectValue(people, "name", 2);
console.log(nameValue); // Output: Jane

```

Whoops! That looks like a complex function with lots of types flying everywhere. Lets break it down bit by bit.

1. The first line of the function contains two generic types `<T, K>` where `T` is an object and has a constraint that it must have an object property of `{id: number }` and `K` is a key property of the object `T` (using the `keyof` operator) and has a constraint that it must only contain properties from `T` object (using the `extend` keyword).

2.   The second line contains the parameters the function receives and what the function returns.
    - `arr`: is an array of our object `T`.
    - `key`: is a key property of object `T`.
    - `id`: is a number unique to every object `T` in the array.
    - The function getObjectValue then returns the value of the object key property found, T[K], or undefined if the value is not found.


3. The third line checks through the array using the `id` as a criterion, finding the object with the same `id.`

4. The fourth line checks if the object `foundObject` we just searched for actually exists and if it does, we extract a value from it using its key `foundObject[key]` and return it, or return `undefined` if its not found.

Thats it. We just broke down the `getObjectValue` function into small chunks to better understand the generic type definitions it has.

## Generics can have default values.

We can also use generics with default values when working with functions. On our previous `getObjectValue` function, imagine wed like to add a default parameter for our `id` parameter as the default value our function filters by. We could rewrite the function like this:

```ts
function getObjectValue<T extends { id: number }, K extends keyof T>
  (arr: T[], key:K, id:number = 2): T[K] | undefined {
  const foundObject = arr.find((obj) => obj.id === id);
  return foundObject ? foundObject[key] : undefined;
}

```

By updating the `id: number` to be `id: number = 2,` we set the default value to be `2` whenever our function runs.

## Conclusion

In conclusion, we covered generics in TypeScript, how they make our code reusable, and how they help to add flexibility to our code using some examples of type declarations and functions.

Generics is one of the features of TypeScript that might look overwhelming at first, but once you get to know it better, you'll appreciate how it makes your code more strictly typed.

I hope this article helps you better understand generics.

Thanks for reading. Cheers.

## References

- [Official TypeScript documentation on generics](https://www.typescriptlang.org/docs/handbook/2/generics.html)

I'd love to connect with you on [**Twitter**](https://twitter.com/0xJamin) | [**LinkedIn**](https://www.linkedin.com/in/trust-jamin/) | [**GitHub**](https://github.com/0xJamin/)