Use Generics in TypeScript
In the previous part
of this article and video series you learned about various types of decorators.
Continuing our learning further this part discusses how generics can be utilized
in TypeScript. If you are a C# developer chances are you already know generics.
You can use that concept in TypeScript also. To that end this article discusses
a few examples that show how to put generics to use in TypeScript classes and
methods.
Suppose you are dealing with an application that needs to export certain data
to PDF format. The data to be exported resides in Order object. You wrote a
component that processes the Order objects and does the export to PDF work
for you. This means the code you wrote was essentially for Order type. Now
suppose that another object, say Customer, also needs to be exported to PDF
format. The "magic" of data export to PDF is going to be similar but since the
earlier code was written for Order type, you need to repeat the same code for
Customer type. Wouldn't it be nice if we can parameterize the type being used by
our data export logic? That's where generics can be used. Using generics you can
create classes and methods that can work with variety of types.
Let's understand this with an example.
Add a new TypeScript file called Generics.ts in your ASP.NET Core project.
Then add the following classes to Generics.ts file :
class Employee {
employeeID : number;
fullName : string;
constructor(id, name) {
this.employeeID = id;
this.fullName = name;
}
showDetails() {
alert(`Employee #${this.employeeID}
- ${this.fullName}`);
}
}
class Customer {
customerID: number;
companyName: string;
contactName: string;
constructor(id, company,contact) {
this.customerID = id;
this.companyName = company;
this.contactName = contact;
}
showDetails() {
alert(`Customer #${this.customerID}
- ${this.companyName}`);
}
}
As you can see, there are two classes Employee and Customer with their own
set of properties, constructor, and showDetails() method.
Now, suppose that you want to pass the data represented by Employee and
Customer objects to the sever through Web API call.
At first glance you would probably create two classes that do the job :
class EmployeeDataProcessor {
process(obj: Employee) {
// send to server
}
}
class CustomerDataProcessor {
process(obj: Customer) {
// send to server
}
}
Although we won't write any real logic to send data to server, the process()
method does that by accepting the respective objects and then processing them as
required.
Assume that the overall processing logic is same for both the process()
methods we can use generics to parameterize the types involved (Employee and
Customer in this case).
class DataProcessor<T>{
process(obj: T) {
//send to server
}
}
The DataProcessor now uses generics syntax of TypeScript - <T> - to
parameterize the type passed to the DataProcessor class. Inside, the process()
method accepts an object of type T. Depending on the T passed it could be
Employee, Customer, or any other type.
To use the DataProcessor class you would write the following code :
let emp : Employee = new Employee(1, 'Nancy');
let cust: Customer = new Customer(2,
'Some company here' ,'Andrew');
let empProcessor:DataProcessor<Employee> =
new DataProcessor<Employee>();
empProcessor.process(emp);
let custProcessor: DataProcessor<Customer> =
new DataProcessor<Customer>();
custProcessor.process(cust);
As you can see, the above code creates an Employee and Customer object by
passing initial values in the constructor. It then creates an object of
DataProcessor by specifying T to be Employee. This means DataProcessor and
process() are going to deal with Employee type.
Similarly, another DataProcessor object is created by specifying T to be
Customer. This time DataProcessor and process() expect objects of Customer.
If you try to pass a Customer object to empProcessor, TypeScript gives an
error as shown below:
In the preceding example, the process() method didn't access any members
(properties or methods) of the passed T object. If you want process() to access
them you should define an interface that wraps the common members. For example,
Employee and Customer both have a method named showDetails(). If you want to
call showDetails() inside process() you need to first define it in an interface
:
interface IDataObject {
showDetails(): void;
}
Then you can implement IDataObject on Employee and Customer:
class Employee implements IDataObject {
...
}
class Customer implements IDataObject {
...
}
Now you can add a constraint in the DataProcessor class that the T must
implement IDataObject interface.
class DataProcessor<T extends IDataObject>{
process(obj: T) {
obj.showDetails();
// send to server
}
}
Notice the code shown in bold letters. The class now specifies that T should
be a type that "extends" IDataObject. Once this constraint is specified you can
access showDetails() inside process() method.
At times you need to create objects of T inside the generic class or method.
You would expect that the following code would do the trick:
createObject<T>(): T {
let obj = new T();
return obj;
}
But the above code won't work as you might expect. TypeScript will give the
following error :
What's the solution? To correct this problem you need to pass the constructor
function of the type (Employee and Customer in this case) to the method creating
the object. The following code will make it clear:
class DataObjectFactory{
create<T>(constructor:
{ new(...args: any[]): T }, ...ctrArgs): T {
let obj = new constructor(...ctrArgs);
return obj;
}
}
The DataObjectFactory class contains a method called create(). The create()
method is a generic method that takes T as the type parameter. More important is
the first parameter of the create() method. This parameter represents the
constructor of the type. The constructor can take zero or more arguments (for
example, Employee constructor takes 2 parameters and Customer constructor takes
3 parameters). The second parameter of the create() method indicates the values
to be passed to the constructor. The create() method returns an object of T
Inside, the code creates an object of T by calling new on the constructor and
by passing the parameters to the constructor. The object is then returned to the
caller.
You can use DataObjectFactory class like this:
let factory = new DataObjectFactory();
let emp: Employee = factory.create
(Employee, 1, 'Nancy');
let cust: Customer = factory.create
(Customer, 2, 'Some company here', 'Andrew');
emp.showDetails();
cust.showDetails();
If all goes well you will see showDetails() displaying the respective
values.
I hope you got some idea about use of generics in TypeScript. You can also watch the companion video
here.
That's it for now! Keep coding!!