Learn ASP.NET Core 3.1 with mini project : MVC, Razor Pages, Web API, Entity Framework Core, and Blazor. Hands-on online training course starting in August 2020. Click here to know more.

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!!


Bipin Joshi is an independent software consultant, trainer, author, yoga mentor, and meditation teacher. He has been programming, meditating, and teaching for 24+ years. He conducts instructor-led online training courses in ASP.NET family of technologies for individuals and small groups. He is a published author and has authored or co-authored books for Apress and Wrox press. Having embraced the Yoga way of life he also teaches Ajapa Yoga to interested individuals. To know more about him click here.

Get connected : Facebook  Twitter  LinkedIn  YouTube

Posted On : 01 June 2020


Tags : ASP.NET ASP.NET Core MVC .NET Framework C# Visual Studio


Subscribe to our newsletter

Get monthly email updates about new articles, tutorials, code samples, and how-tos getting added to our knowledge base.

  

Receive Weekly Updates