How often do you come across with situation where you have to duplication code in order to achieve the same data but with different types? Sometimes we use the same logic in different classes or functions in order to work with data connected with specific type. Generic type was created in order to solve this problem, and in this article we will dive deep in understanding generics in typescript.
Generic type in classes
Classic approach
Let's start with some example, we want to create class for receiving user’s data . Let’s declare the user mock class:
export class User {
constructor(
public id = 1,
public firstName = 'Maksim',
public lastName = 'B') {
}
}
It’s just a simple class where we specify public values through constructor.
Then we need an interface where we define methods that should be used in our User class.
interface UserService {
get: (id: number) => User;
add: (user: User) => boolean;
update: (user: User) => boolean;
delete: (id: number) => boolean;
}
So It’s a good time to create a class itself:
class Users {
constructor(private userService: UserService) {
}
get(id: number): User {
return this.userService.get(id);
}
add(user: User): boolean {
return this.userService.add(user);
}
update(user: User): boolean {
return this.userService.update(user);
}
delete(id: number): boolean {
return this.userService.delete(id);
}
}
And this is a fully working example which will return the data specific for User class. Developers can get, add, update and delete users with this proxy class.
But let’s imagine that in our application we want to receive an Employee or Seller or something else. Keeping that approach we will repeat everything in this class excepting types of data that being passed or received.
Generic approach
So if we know that type of provided service and returned data should be variable.
In Typescript we have Generic
- is a dynamic type which could be as an extension of another type. Generic approach is often used in Angular application, if you want to know more about angular development, you can check out my angular articles.
In many other languages which support generics the default declaration of type is T
, but we can also use other letters.
For generic types we can check whether generic type is an instance of inference or another type, and many other useful things that definitely helps with creating robust application.
The typical declaration of generic class looks like this:
class Example‹T› {
constructor(private data: T[]) {
}
get(index: number): T {
this.data[index]
}
}
This record doesn’t mean that type T
is an any
, it means that type should be specified when new instance of a class being created. For example:
const user: User = new Example‹User›([new User()]).get(0);
So here we receive user as a result of get function in Example class in spite of the fact that in our class we never mentioned the User interface.
So it’s a good time to write our service in a generic way! First of all we have to specify generic interface for our generic service:
interface CommonService‹T› {
get: (id: number) => T;
add: (value: T) => boolean;
update: (value: T) => boolean;
delete: (id: number) => boolean;
}
Here we write an abstract name for our function signature, because it might be many types of data.
The next step is declaring our generic class:
class Service ‹T extends CommonService‹U›, U› {
constructor(private service: T ) {
}
get(id: number): U {
return this.service.get(id)
}
add(data: U): boolean {
return this.service.add(data);
}
update(data: U): boolean {
return this.service.update(data);
}
delete(id: number): boolean {
return this.service.delete(id);
}
}
As you might noticed in class declaration we have used 2 generic types T
and U
for make an abstract service T
, and returned type U
. Make notice that in order to have CommonService methods we tell Typescript that type T
is an extension of type CommonService
.
Now we have universal class where we can send many types of classes which implements CommonService interface and return value of a specific type.
It’s time to try! We need another class for tasting our generic service for returning values. In our example it will be a Seller
class.
class Seller {
constructor(public id = 1, public accountNumber = 123) {}
}
And create new instances of generic service and receive data with uniq types:
const user = new Service‹CommonService‹User›, User›({get: (id: number) =› new User()} as CommonService‹User›).get(1);
const user1 = new Service‹CommonService‹User›, User›({get: (id: number) =› new User(1)} as CommonService‹User›).get(1);
const seller = new Service‹CommonService‹Seller›, Seller›({get: (id: number) =› new Seller()} as CommonService‹Seller›).get(1);
const seller1 = new Service‹CommonService‹Seller›, Seller›({get: (id: number) =› new Seller(1)} as CommonService‹Seller›).get(1);
seller.accountNumber;
user.id;
So now we have instances of User and Seller class with different properties and we got it from the same class!
Generic type in functions
Traditional approach
The same functionality may be used with functions, so let’s start from example. Sometimes we need users hash where user’s id will be the key and the user model as a value. In order to active this we will write following function:
import { User } from './user-service';
export const getUsersHash = (users: User[]) => {
return users.reduce((acc, c) => {
acc[c.id] = c;
return acc;
}, {} as Record<string, user="">)
}</string,>
In this function we receive users array as an input and the output will be user’s hash.
But as you may notice this functionality could be repeated with any models that have id
field, so it will be too repetitive to write this function for all models that contains this field.
Generic way
Let’s rewrite the same function using generics way. Firstly we know that every receiving object should has an id
field, and not only User’s model may have this field. Much better approach is declaring new interface which has only one key - id
.
export interface Hashable {
id: number;
}
Then we can specify input type as an extension of Hashable interface and output value as a generic type, like this:
export const getHash = ‹T extends Hashable›(data: T[]) => {
return data.reduce((acc, c) => {
acc[c.id] = c;
return acc;
}, {} as Record‹string, T›)
}
A little bit examples:
const usersHash = getUsersHash([user, user1]);
const usersGenericHash = getHash‹User›([user, user1]);
Source code
Conclusion
Now we know that generics can help with duplication code and extends functionality of our classes, functions, interfaces. The default of generic type abbreviation is T
symbol. And generic types can not only be used by itself they can be used as extension functionality in a signature.
Hope this information was helpful for you.