Understanding Typescript Generics and How to Implement Them

Written by amarachi | Published 2022/02/01
Tech Story Tags: typescript | typescript-tutorial | typescript-interface | web-development | what-is-typescript | javascript | blogging-fellowship | hackernoon-top-story

TLDRTypescript Generics are a way to define flexible variables, functions, objects, arrays etc. They are a stricter, more syntactical superset of [JavaScript] The language has become quite popular for a number of reasons, some of which are: easier to read and understand. Sometimes we need a particular function or object to work with a wide range of types and not be restricted by the rigidity that comes with writing with typescript. We trade-off the type definition for flexibility by using the `any` keyword like the code block below.via the TL;DR App

Typescript has taken the development world by storm and has been adopted by most developers and startups across the globe.

Loosely explained as a stricter, more syntactical superset of JavaScript, it has become quite popular for a number of reasons, some of which are:

  • It makes our code easier to read and understand.
  • It saves us from running into painful bugs, as we know what value and type are entering our function and what value and type we are expected to receive.

To help us get more acquainted with the programming language, we’ll explain what typescript generics are and how to implement them in our projects (with examples where they would come in handy).

Table of Contents

  1. What are Typescript Generics?
  2. Why use them?
  3. Working with Typescript Generics on Arrays
  4. Interface
  5. Classes
  6. Typescript Generics Constraints
  7. Conclusion
  8. Resources

What are Typescript Generics?

Typescript Generics are a way to define flexible variables, functions, objects, arrays etc. Sometimes we need a particular function or object to work with a wide range of types and not be restricted by the rigidity that comes with writing with typescript. That's where typescript generics come in.

Why use them?

What if we wanted to define a function that gets the user's id, and we are not sure if the id will be a string or an integer? What do we do then? We could either define multiple functions to take care of the different types or use the any keyword like the code block below.


function getId(arg: any): any  {
   returns arg;
}
 
let id1 = getId('Amara')
 

The above function would work in our project and not throw off any error; however, we trade-off the type definition for flexibility by using the any type. When an argument is passed in, we have no idea what the type of the argument is and what type the function returns.

These are all the issues that typescript generics solve.

function getId<Type>(arg: Type): Type {
    return arg;
}
 
let userOne = getId<String>('identifier1')

We add a Type variable, then pass Type as the type of the argument, and finally, specify that the function will return a value of Type.

This means that the argument’s type is also the value type the function returns.

NOTE: Type is just a placeholder and not the standard, as we can replace it with any other string, e.g. <T>, <ReturnType> etc.

Creating a userOne function, we then explicitly state that the type of the argument is a string and that the function should also return a string.

When we hover over userOne we can see that we get a type of string, and that is due to the magic of typescript generics.

Working with Typescript Generics on Arrays

function getLastItem<Type>(arr: Type ): Type{
    return arr[arr.length -1]
}
 
let arr1 = getLastItem([1,2,3,4,5])
console.log(arr1)

When we run this program, we get an error because the function is flexible but doesn’t know the type of argument we are looking to pass in and therefore doesn’t know that it has a length property.

We can resolve this by specifying that we want a generic array type.

function getLastItem<Type>(arr: Type[] ): Type{
    return arr[arr.length -1]
}
 
let arr1 = getLastItem([1,2,3,4,5])
console.log(arr1)// returns 5

The [] specifies that the argument will be an array typescript, knowing that there usually is a length property on the array allows us to do this.

Interface

Interfaces define the behaviors of an object.

interface Person<T,U>{
    (firstname: T, age: U ) : void;
};
 
function introducePerson<T, U>(firstname: T, age: U): void{
    console.log('Hey! My name is ' + firstname + ',and I am ' + age);
}
 
let person1: Person<string, number> = introducePerson;
person1("Amara", 12); // Output: Hey! My name is Amara, and I am 12

In the code block above, we declared a generic interface called Person with two properties, firstname and age. These properties accept generic arguments that we represent with the type variables <T> and <U>.

With this generic interface, we can then use any method with the same signature as the interface, such as the introducePerson method.

Creating the person1 object, we state that the object has a type of the Person interface, signifying that person1 will have a firstname and age property. We then define the argument types getting passed in (string and number). Equating it to the introducePerson allows us to have access and eventually run the function.

Classes

Classes are explained as blueprints or templates used to create objects.

class Collection{
    items: Array<number> = [];
   
    add(item: number) {
        this.items.push(item);
    };
 
    remove(item: number){
        const index = this.items.findIndex(i=> i === item);
        this.items.splice(index, 1);
        return this.items
    }
 
}
 
const myCollection = new Collection();
myCollection.add(1);
myCollection.remove(1);

In the example above, we created a class called Collection. This class contains an items array and two functions, add and remove, to add and remove an item from the items array.

We then create an instance from the Collection class myCollection, and finally, call the add and remove method.

This is great, right? However, this class and any instance created from this class can only work with the number type, and anything else will throw an error.

class Collection<T>{
    items: Array<T> = [];
   
    add(item: T) {
        this.items.push(item);
    };
 
    remove(item: T){
        const index = this.items.findIndex(i=> i === item);
        this.items.splice(index, 1);
        return this.items
    }
 
}
 
const myCollection = new Collection<number>();
myCollection.add(1);
myCollection.remove(1);

In the block of code above, we use generic types when creating the class Collection, so now create different instances that work with different types.

Typescript Generics Constraints

When working with generics in typescript, we might want more specificity than just the type of argument a function is looking to take in. With typescript generics constraints, we can decide to reject an argument not just because it is not an object but also because the object does not have specific keys or parameters in it.

function addAge <T extends {name: string}>(obj: T) {
    let age = 40;
    return {... obj, age};
}
 
let docOne = addAge({name: 'Amara',color: 'red'});
let docTwo = addAge({series: 'killing eve',color: 'red'  }); // throws an error
 
console.log(docOne);

In the block of code above, we use the extends keyword to specify that we not only need the argument to be of an object type but that the object needs to have a name property. If we replaced the name property from the argument passed into docOne, we would have an error.

We could also add constraints using interfaces.

Recall an earlier example where we tried to find the last number in the array using the .length property but got an error. We then worked around it by explicitly stating that we only accepted array arguments.

With generics constraints, we could decide not to be limited to only arrays but accept any argument as it possesses the length property.


interface LengthOfArg {
  length: number;
}
 
function logLength<Type extends LengthOfArg>(arg: Type): Type {
  console.log(arg.length); 
  return arg;
}

logLength({length: 52, name: 'Amara'})// 52
logLength([1,2,3])//3
logLength('Orange')// 6
logLength(2)// throws off an error

In the block of code above, we use the extends keyword to specify that we want our argument to have the length property on the interface LengthOfArg. Doing this allows us to pass in an object containing a length property, an array, or a word and have our function not throw an error, this is because they all have a length property on them.

Conclusion

This article discussed typescript generics, why we would need them in our code, and, more importantly, how to implement them.

Resources

You may find the following resources helpful.


Written by amarachi | Frontend Developer and Technical Writer
Published by HackerNoon on 2022/02/01