Generics in Typescript

Avishek Patra
6 min readMar 14, 2020

Introduction

A major part of software engineering is building components that not only have well-defined and consistent APIs but are also reusable. Components that are capable of working on the data of today, as well as the data of tomorrow, will give you the most flexible capabilities for building up large software systems.

In languages like C# and Java, one of the main ways for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

First Implementation of “Generics”

Let’s start off with a very implementation of generics in a typescript program. The prerequisite for this tutorial is the following

i. Node installation

ii. Typescript installation

iii. Any editor, we will be using vs-code for this tutorial.

So we will be creating a function which will be returning the parameter that is passed as an argument. So without generics, the implementation function either should have a specific type like this

function returnInput(arg: number){
return arg;
}

Or it can be written with the use of any type

function returnInput(arg: any): any {
return arg;
}

While using any is certainly generic in that it will cause the function to accept any and all types for the type of arg, we actually are losing the information about what that type was when the function returns. If we passed in a number, the only information we have is that any type could be returned.

Instead, we need a way of capturing the type of argument in such a way that we can also use it to denote what is being returned. Here, we will use a type variable, a special kind of variable that works on types rather than values.

function returnInput<T>(arg: T) : T{
return arg;
}

We’ve now added a type variable T to the returnInput function. This T allows us to capture the type the user provides (e.g. number), so that we can use that information later. Here, we use T again as the return type. On inspection, we can now see the same type is used for the argument and the return type. This allows us to traffic that type information in one side of the function and out the other.

Let’s look at the following piece of code

function returnInput<T>(arg: T): T{
return arg;
}
//Output type will be a string
let output = returnInput<string>('Avishek');

Here we explicitly set T to be string as one of the arguments to the function call, denoted using the <> around the arguments rather than ().

The second way is also perhaps the most common. Here we use type argument inference — that is, we want the compiler to set the value of T for us automatically based on the type of argument we pass in:

let output = returnInput('Avishek');

Here we didn’t have to explicitly pass the type in the angle brackets (<>); the compiler just looked at the value "Avishek", and set T to its type. While type argument inference can be a helpful tool to keep code shorter and more readable, you may need to explicitly pass in the type arguments as we did in the previous example when the compiler fails to infer the type, as may happen in more complex examples.

Generic Type Variable

When we create a function like “returnInput”, the compiler will enforce that you use any generically typed parameters in the body of the function correctly. That is, that you actually treat these parameters as if they could be any and all types.

Let’s look at returnInput function from earlier

function returnInput<T>(arg: T): T{
return arg;
}

What if we want to also log the length of the argument arg to the console with each call? What if we write this

function returnInput<T>(arg: T): T{
console.log(arg.length);
return arg;
}

When we will compile the above program we should get an error like this

An error occurring on the compilation of the program

So how does it happening, Remember, we said earlier that these type variables stand-in for any and all types, so someone using this function could have passed in a number instead, which does not have a .length member.

Let’s say that we’ve actually intended this function to work on arrays of T rather than T directly. Since we’re working with arrays, the .length member should be available. We can describe this just like we would create arrays of other types:

function returningInputs<T>(arg: T[]): T[] {
// Array has a .length, so no more error
console.log(arg.length);
return arg;
}

You can read the type of returningInputs as “the generic function returningInputs takes a type parameter T, and an argument arg which is an array of Ts, and returns an array of Ts.” If we passed in an array of numbers, we’d get an array of numbers back out, as T would bind to number. This allows us to use our generic type variable T as part of the types, we’re working with, rather than the whole type, giving us greater flexibility.

We can alternatively write the sample example this way:

function returningInputs<T>(arg: Array<T>): Array<T> {
// Array has a .length, so no more error
console.log(arg.length);
return arg;
}

Generic Types

In this section, we’ll explore the type of the functions themselves and how to create generic interfaces.

The type of generic functions is just like those of non-generic functions, with the type parameters listed first, similarly to function declarations:

function genericType<T>(arg: T): T {
return arg;
}
let myType: <T>(arg: T) => T = genericType;

We could also have used a different name for the generic type parameter in the type, so long as the number of type variables and how the type variables are used line up.

function genericType<T>(arg: T): T {
return arg;
}
let myType: <U>(arg: U) => U = genericType;

The generic types can also be written as a call signature of an object literal type

function genericType<T>(arg: T): T {
return arg;
}
let myType: {<T>(arg: T): T} = genericType;

Now Let’s take an object literal form and move it to an interface which will lead us to write our first generic interface in typescript

interface MyGenericInterface{
<T>(arg: T): T;
}
function genericType<T>(arg: T): T {
return arg;
}
let myType: MyGenericInterface= genericType;

Or if we may want to write it in the following way we can do that as well.

interface MyGenericInterface{
<T>(arg: T): T;
}
function genericType<T>(arg: T): T {
return arg;
}
let myType: MyGenericInterface<string>= genericType;

Generic Classes

Like interfaces, classes can be generic as well, let's consider the following code snippet

class MyGenericClass<T> {
baseValue: T;
add: (x: T, y: T) => T;
}
let myGenericClass = new MyGenericClass<number>(); myGenericNumber.baseValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

As you can see above the code snippet the type can be passed when instantiating the class, here in the example we have passed “number” for type “T” which means the compiler will resolve the generic type as Number. And instead of the “number”, we could’ve passed the string then the add function will perform concatenation the two string inputs instead of adding them. Let’s look at the following code snippet

let myGenericClass = new MyGenericClass<string>(); myGenericNumber.baseValue = ''; 
myGenericNumber.add = function(x, y) { return x + y; };
console.log(myGenericNumber.add(myGenericNumber.zeroValue, "test"));

One thing to be remembered that the Generic nature of the class will occur only when the class is instantiated. Not before that.

These are the most used generics in the typescript. However, there are a couple of more scenarios of generics like generic constraints, Class Type Generics, Type parameters in generic constraints. So if you want me to write an article on them as well, then let me know in the comments section.

Meanwhile, you can see the codes in the following GitHub repo.

If you love my article and want me to write more then, please press the clap button, it will not help me in earning money but of course give huge moral support to write more such articles.

--

--