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:
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).
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.
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.
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.
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 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.
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.
This article discussed typescript generics, why we would need them in our code, and, more importantly, how to implement them.
You may find the following resources helpful.