DEV Community

loading...
Cover image for Understanding Typescript Generics

Understanding Typescript Generics

tunmisesuliat profile image Tunmisesuliat Updated on ・4 min read

Naturally, Every good software developer looks forward to making their components as reusable as possible. The idea that developers can create reusable components that can be used generally across their entire application, and in a new application is really a big deal as software developers become more concerned about improvements in application development speed, efficiency, and user experience harmonization.
One of the tools for creating such reusable components in Typescript are Generics. Generics allow you to be able to create components that can work over a variety of types, rather than a single one. In simpler terms, Generics gives you the power to be able to create an object e.g a function that can accept, return, and work with different data types without losing the information about what the data type was when the function returns.
To start off, let's give an example of a function that takes an array as an argument, and then for each index of the array, adds up the value of the current and preceding indexes. without generics, we would be doing something like this:

const performOperation =(acc:number[],currentval:number) : number[]=>{
  if (acc.length === 0){
    acc.push(currentval)  
  } else{    
      acc.push((acc[acc.length-1] + currentval)) 
  }
  return acc;
};
const arrayAdder = (arr: number[]) : number[]=> {
return arr.reduce(performOperation, [])
}
console.log(arrayAdder([2, 3, 4]));

Enter fullscreen mode Exit fullscreen mode

Note that without generics, we had to give our functions specific argument and return type. This means that our functions cannot work with any other data type apart from number. a simple solution to this problem might be to describe our functions using the any type.

const performOperation =(acc:any[],currentval:any) : any[]=>{
  if (acc.length === 0){
    acc.push(currentval)  
  } else{    
      acc.push((acc[acc.length-1] + currentval)) 
  }
  return acc;
};
const arrayAdder = (arr: any[]) : any[]=> {
return arr.reduce(performOperation, [])
}
console.log(arrayAdder([2, 3, 4]));

Enter fullscreen mode Exit fullscreen mode

using any to describe our functions is absolutely generic because it allows our functions to accept any and all types of array. But the problem is that we can no longer be sure about the data type when the function array_adder returns. we only know that if we passed in an array of numbers, anything could be returned.
The solution to this problem, is to be able to capture the data type of the argument and also use it to denote what is to be returned. That is exactly the power that generics gives us.

const performOperation = (acc:any[],currentval:any) : any[]=>{
  if (acc.length === 0){
    acc.push(currentval)  
  } else{    
      acc.push((acc[acc.length-1] + currentval)) 
  }
  return acc;
};
const arrayAdder = <T>(arr: T[]) : T[]=> {
return arr.reduce(performOperation, [])
}
Enter fullscreen mode Exit fullscreen mode

We have now added a type variable T to the array_adder function. T helps us to capture the data type of the argument the user provides, so as to be able to reuse it. Hence, we say that the array_adder function is generic as it can work over a range of types. Now, our generic function can be called in two ways, either by passing in the type or by argument inference.

console.log(arrayAdder<number>([2, 3, 4])); // number is passed as the argument type
console.log(arrayAdder<string>(["l", "o", "l"]));
or
console.log(arrayAdder([2, 3, 4])); // the argument type is determined explicitly
console.log(arrayAdder(["l", "o", "l"]));
Enter fullscreen mode Exit fullscreen mode

Generic Types

previously, we created functions that worked with a range of data types. Now we will explore the type of the functions, and learn how to create generic interfaces.

const arrayAdder = <T>(arr: T[]) : T[]=> {
return arr.reduce(performOperation, [])
}
const myArray: <V>(arr: V[]) => V[] = arrayAdder
console.log(myArray<string>(["h", "e", "l", "l", "o"])) //[ 'h', 'he', 'hel', 'hell', 'hello' ]
Enter fullscreen mode Exit fullscreen mode

the above example defines the myArray as being able to hold a function that accepts and returns the same type both specified using the data marker V.
Now, let's go on to write our first generic interface.

interface Genericfunction{
<V>(arr: V[]):V[]
}
const arrayAdder = <T>(arr: T[]) : T[]=> {
return arr.reduce(performOperation, [])
}
const myArray: Genericfunction = arrayAdder
console.log(myArray<string>(["h", "e", "l", "l", "o"])) //[ 'h', 'he', 'hel', 'hell', 'hello' ]
Enter fullscreen mode Exit fullscreen mode

Also, we could move the generic parameter to be a parameter of the whole interface.

interface Genericfunction<V>{
(arr: V[]):V[]
}
const myArray: Genericfunction<string> = arrayAdder
console.log(myArray(["h", "e", "l", "l", "o"])) //[ 'h', 'he', 'hel', 'hell', 'hello' ]
Enter fullscreen mode Exit fullscreen mode

Here, instead of describing a generic function, we have a non-generic function that is part of a generic type.So instead of placing the type on the function call signature, we place it on the interface itself.

Generic Classes

A generic class is similar to a generic interface. Let's create a generic class called addNumber.

class GenericClass<T>{
addNumber: (a:T, b:T)=> T
}
let myGenericNumber = new GenericClass<number>()
myGenericNumber.addNumber = (x, y) => x + y;

Enter fullscreen mode Exit fullscreen mode

Just like in interfaces, adding a type parameter on the class ensures that all properties of the class are working with the same type.

Conclusion

Generics is a handy tool when it comes to writing reusable and maintainable code. In the long term, not only will you benefit from it, but your whole team will. your codebase becomes readable, and maintainable. that means other developers can easily understand and maintain it if needed as it makes your application more consistent.

Discussion (0)

pic
Editor guide