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"
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
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" });
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}
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 }
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>;
}
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)
}
}
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;
}
}
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
);
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)