Master "Generics" In Typescript🎉
Tags: typescript, webdev, programming, javascript
Published: 2023-05-11 09:44:43

Generics in Typescript allows you to create a component that can work with different types. This lets users use these components with their specific type.

**_Before reading this article, please read my article about TS [utility types](https://dev.to/arafat4693/typescript-utility-types-that-you-must-know-4m6k) as It will help you to understand this article better._**

## Table of contents

- [1. Generics with type](#1-generics-with-type)
- [2. Generic functions](#2-generic-functions)
- [3. Generics with built-in functions](#3-generics-with-built-in-functions)
- [4. Inferring the types](#4-inferring-the-types)
- [5. Constraints on type arguments](#5-constraints-on-type-arguments)
- [6. Constraints in functions](#6-constraints-in-functions)
- [7. Use `as` when necessary](#7-use-as-when-necessary)
- [8. Multiple generics](#8-multiple-generics)
- [9. Defaults in type arguments](#9-defaults-in-type-arguments)
- [10. Class Types in Generics](#10-class-types-in-generics)
- [Conclusion](#conclusion)

> Here are few possible ways to use `Generics` in Typescript:

## 1. Generics with type
The easiest way of using `generics` is with `type.` For example:

```ts
type MyData<Type> = {
  data: Type
}

type DataAsString =  MyData<string>;
// type DataAsString = {
//    data: string;
// }
```
Because we passed `string` as a type parameter, our data will be `string` type. Likewise, if we pass the `number` type, our data will be of number type. And that's the beauty of `generics.` You can work with any type.

```ts
type MyData<Type> = {
  data: Type
}

type DataAsString =  MyData<number>;
// type DataAsString = {
//    data: number;
// }
```

## 2. Generic functions
As you can pass arguments to a function, you can also pass `type arguments` to a function. For example:

```ts
const fetchData = <Type>(url: string): Promise<Type> => {
    return fetch(url)
}

fetchData<{name: string, age: number}>("/api/books")
  .then(res => {
      console.log(res)
      // res: {name: string, age: number}
  })
```
We passed `{name: string, age: number}` as a `type argument` to the `fetchData` function, And this type argument tells `res` what It is supposed to be because this is what's getting typed in `Promise<Type>.` So now the function `fetchData` has become a generic function. A generic function is just a normal function but includes a type argument.

If we want to pass another type, our `res` will replicate that same type.

```ts
const fetchData = <Type>(url: string): Promise<Type> => {
    return fetch(url)
}

fetchData<{id: number, email: string}>("/api/books")
  .then(res => {
      console.log(res)
      // res: {id: number, email: string}
  })
```

## 3. Generics with built-in functions
You can even use generics with built-in functions. For example, you can create a `Set` that stores only numbers by using the Set constructor with a type parameter:

```ts
const numberSet = new Set<number>();
numberSet.add(1);
numberSet.add(2);
numberSet.add(3);
```
Similarly, you can create a `Map` that maps strings to numbers by using the Map constructor with type parameters for the key and value types:

```ts
const stringToNumberMap = new Map<string, number>();
stringToNumberMap.set("one", 1);
stringToNumberMap.set("two", 2);
stringToNumberMap.set("three", 3);
```

## 4. Inferring the types
If your `type argument` looks similar to your `runtime argument` then you don't have to pass a `generic type` to your function. For example:

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

const result1 = identity<number>(42); // const result1: number
const result2 = identity<string>("hello"); // const result2: string
```
While the above example looks correct, we can simplify it more:

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

const result1 = identity(42); // const result1: number
const result2 = identity("hello"); // const result2: string
```
In this case, because we are not passing any `type argument`, then `Typescript` will look in the `runtime argument` to see If It can `infer` anything from it.
So the return type is `inferred` from the runtime argument type, and the function returns the same type as it receives.

## 5. Constraints on type arguments
In our following example, we wanted to be able to use the `ReturnType` utility type, but the compiler could not prove that every type was a function; it could be a string or number, and as we know, that `ReturnType` works only with functions, so it warns us that we can’t make this assumption.

```ts
type GetPromiseData<T> = Awaited<ReturnType<T>>
// Error: Type 'T' does not satisfy the constraint '(...args: any) => any'

type PromiseResult = GetPromiseData<
  () => Promise<{
    id: string
    email: string
  }>
>
```
So to make It clear to Tyepscript, We have to say that `T` is only a function, and we can do this by adding `extends (...args: any) => any` after the `T`:

```ts
type GetPromiseData<T extends (...args: any) => any> = Awaited<ReturnType<T>>

type PromiseResult = GetPromiseData<
  () => Promise<{
    id: string
    email: string
  }>
>
```

## 6. Constraints in functions
In our following example, we wanted to access the `.address` property of arg, but the compiler could not prove that every type has a `.address` property, so it warned us that we couldn’t make this assumption.

```ts
function myFunc<Type>(arg: Type): Type {
  console.log(arg.address); // Property 'address' does not exist on type 'Type'.
  return arg;
}
```
To fix this error, we can create an interface with `address` property in it and extend it with the `Type` argument:

```ts
interface IDetails {
  address: string;
}
 
function myFunc<Type extends IDetails>(arg: Type): Type {
  console.log(arg.address); // Now we know it has a .address property, so no more error
  return arg;
}
```
Because the generic function is now constrained, it will no longer work over all types:

```ts
myFunc(true);
// error: Argument of type 'boolean' is not assignable to parameter of type 'IDetails'.
```
Instead, we need to pass in values whose type has `address` property:

```ts
myFunc({ length: 10, address: "Something" });
```

## 7. Use `as` when necessary
Sometimes using `as` is the best thing you can do when using generics. For example:

```ts
const ObjectKeys = <T extends {}>(obj: T): Array<keyof T> => {
  return Object.keys(obj) // Error: Type 'string[]' is not assignable to type '(keyof T)[]'. 
}

const result = ObjectKeys({
  id: 6,
  email: "me@gmail.com"
})
```
Here we are getting an error because the `Object.keys` method returns an array of string values, and TypeScript cannot guarantee that the string values returned by `Object.keys` actually correspond to valid keys of the generic type `T.`

To fix this error, we can explicitly typecast the `Object.keys` result to an array of keyof `T` using the `as` keyword:

```ts
const ObjectKeys = <T extends {}>(obj: T) => {
  return Object.keys(obj) as Array<keyof T>
}

const result = ObjectKeys({
  id: 6,
  email: "me@gmail.com"
})
```

## 8. Multiple generics
Sometimes we have to use multiple generics to make sure that we are getting back a certain type. Consider the following example:

```ts
function getProperty<T>(obj: T, key: keyof T) {
  return obj[key];
}
 
let x = { a: 1, b: "b", c: true, d: 4 };
 
getProperty(x, "a"); // return type: string | number | boolean
```
While the above example is correct, the problem is that the return type is not explicit. Return type is an `union` consisting of 3 different types. To fix this problem, we can use multiple generic types:

```ts
function getProperty<T, Key extends keyof T>(obj: T, key: Key) {
  return obj[key];
}
 
let x = { a: 1, b: "b", c: true, d: 4 };
 
getProperty(x, "a"); // return type: number
```
In this example, we've added generic type parameter `Key` to represent the key type of the object `T`. So now we will get a specific return type only.

```ts
function getProperty<T, Key extends keyof T>(obj: T, key: Key) {
  return obj[key];
}
 
let x = { a: 1, b: "b", c: true, d: 4 };
 
getProperty(x, "c"); // return type: boolean
```

## 9. Defaults in type arguments
We can also use default types in generics. Consider the following example:

```ts
const makeSet = <T>() => {
  return new Set<T>()
}

const mySet = makeSet() // const mySet: Set<unknown>
```
Here we are getting `unknown` because we didn't pass any type argument to `makeSet` function. We can solve this problem by either passing a type argument like this, `makeSet<number>()` or by specifying a default type:

```ts
const makeSet = <T = number>() => {
  return new Set<T>()
}

const mySet = makeSet() // const mySet: Set<number>
```

## 10. Class Types in Generics
We can also refer to class types by their constructor functions. For example:
```ts
function create<Type>(c: { new (): Type }): Type {
  return new c();
}
```
Here's a breakdown of how the function works:

- The `create` function is declared with a type parameter `Type`, representing the type the function will make.


- The function takes a single argument `c`, an object representing a constructor function for the type `Type`. The argument has the type `{ new (): Type }`, an object type specifying a constructor function that takes no arguments and returns a value of type `Type`.


- The `new` keyword is used to create a new instance of the type `Type` inside the function by calling the constructor function passed as the argument `c`. The `new` keyword creates a further object of the type `Type` and returns it as the result of the function.


- The function's return type is specified as `Type`, which ensures that the function returns an instance of the type specified by the type parameter.


Here's an example of how the `create` function can be used:

```ts
class MyClass {
  constructor(public value: string) {}
}

const instance = create(MyClass);
console.log(instance.value); // Output: undefined
```
This example defines a simple class, `MyClass,` with a single property value. We then call the `create` function, passing the `MyClass` constructor function as the argument. The `create` function creates a new instance of `MyClass` using the constructor function and returns it as an instance of type `MyClass.`

---

## Conclusion
In conclusion, TypeScript's support for generics provides a powerful tool for writing type-safe and reusable code. By defining generic types and functions, you can create code that works with various types while maintaining strict type-checking.

To make the best use of generics in TypeScript, it's essential to understand how to define and use generic types, specify constraints on generic type parameters, and use type inference to reduce the need for explicit type annotations. Additionally, it's crucial to use generics to maintain good code readability and avoid unnecessary complexity.

When appropriately used, generics can significantly improve the quality and maintainability of your TypeScript code. By taking advantage of TypeScript's powerful type system and the flexibility of generic types, you can create highly reusable and expressive code while still maintaining the strong type safety that TypeScript provides.

**Visit**:
👨‍💻[My Portfolio](https://portfolio-khaki-iota-89.vercel.app/)
🏞️[My Fiverr](https://www.fiverr.com/sunnyislam990)
🌉[My Github](https://github.com/arafat4693)
🧙‍♂️[My LinkedIn](https://www.linkedin.com/in/arafat-freelancer)