TypeScript Tutorial: How to Make Code Reusable
In this TypeScript tutorial, the last in a three-part series, we explain how to adhere to the DRY principle — making TypeScript code reusable — using generics and namespaces.
October 16, 2018
This article is the third part of a three-part TypeScript tutorial. In the first part, we covered the basic type system in TypeScript and looked at the primitive data types available. In the second part we covered custom types such as classes and interfaces and also learned about TypeScript functions. In this part of the TypeScript tutorial, we’ll focus on how to make TypeScript code reusable by using concepts like Generics and Namespaces.
Generics
In a real-world scenario, an application built with TypeScript will communicate with some kind of API for performing CRUD (create, read, update and delete) operations. Let’s assume that you are creating an Angular app that displays information about some products and orders submitted by users. In such a scenario, you would typically create a service class that would perform CRUD operations for a specific feature. For example, you would create classes called ProductsService and OrderService that focus on managing products and orders, respectively.
Life Without Generics
Let’s take a look at the ProductsService class, which would make use of Angular’s HTTPClient to make calls to the API.
export class ProductsService {
private readonly httpClient: HttpClient;
constructor(httpClient: HttpClient) {
this.httpClient = httpClient;
}
public getProducts(): Array {
return new Array();
}
public getProductById(id: number): Products {
return new Products();
}
public postProduct(product: Products): void {
// make an HTTP POST request to your API.
}
public deleteProduct(id: number): void {
// make an HTTP DELETE request to your API.
}
}
Now let’s assume that the app demands displaying order information to users. In that case, you would create an OrdersService class for managing orders. But as the application grows, you keep creating many such service classes that perform CRUD operations for each feature. This is a problem because it means repeating the same code again and again; eventually, you reach a stage where the code base becomes very rigid.
Generics Classes
Development best practices (the DRY, or “don’t repeat yourself,” principle) dictate against repeating code. In TypeScript, generics helps developers adhere to the DRY principle. Let’s learn how the above code, using the HTTPClient for performing CRUD operations, can be reduced from multiple service classes to a single class and how the respective service classes (ProductsService and OrdersService) can make use of that generic class. Here ’s a simple example.
export class Api {
private readonly httpClient: HttpClient;
constructor(httpClient: HttpClient) {
this.httpClient = httpClient;
}
public getMany(): Array {
// make an HTTP GET request to your API.
}
public getById(id: number): T {
// make an HTTP GET request to your API.
}
public post(product: Products): void {
// make an HTTP POST request to your API.
}
public delete(id: number): void {
// make an HTTP DELETE request to your API.
}
}
The ProductsService class can simply make use of the generic API class and alleviate worry about the HTTPClient code. Any number of service classes that you might create could use the API class.
Generic Methods
Similar to generic classes, you can also create generic functions in TypeScript. The above API class can be changed to a simple non-generic class with generic functions. Take a look at the code below.
export class Api {
private readonly httpClient: HttpClient;
constructor(httpClient: HttpClient) {
this.httpClient = httpClient;
}
public getMany(): Array {
// make an HTTP GET request to your API.
}
public getById(id: number): T {
// make an HTTP GET request to your API.
}
public post(object: T): void {
// make an HTTP POST request to your API.
}
public delete(id: number): void {
// make an HTTP DELETE request to your API.
}
}
While using the generic function, TypeScript will automatically infer the return type if you do not specify the generic type argument. Here’s a screen shot demonstrating the same.
ProductsService Class
Changing the return type from Products to Discounts for the getById() function automatically infers the return type to be Discount object.
Generic Constraints
In some cases, you’ll want to add constraints to allow a specific type to be passed as a generic argument. For example, in the API class example above, you can pass any type (primitive data types like numbers, strings, booleans or a custom type like the Products class or an interface). Let’s assume that you want the generic class or function to accept only those types that conform to a condition. In such cases, you will have to add a generic constraint. Here’s how to do it.
First, create a contract, which in simple terms means creating an interface.
interface IGenericConstraint {
hasPassedValidation: boolean;
}
The API class generic functions argument should conform to the contract. To do that, the code should look like this:
export class Api {
public getMany(): Array {
// make an HTTP GET request to your API.
}
}
The ProductsService class should use the Products class to implement that interface; if not, you’ll get a compilation error.
Once you implement the IGenericConstraints interface in the Products class, you can start passing Products as a generic type argument.
Namespaces
Last in the tutorial on TypeScript is a discussion of namespaces.
As an application grows, it becomes important for developers to organize and manage their code. In JavaScript, that’s done using modules; in TypeScript, that’s done using namespaces. A namespace ensures that components are isolated. To allow those components to be used by external features, you must export namespaces by using the export keyword. Here’s a simple example of using namespaces in TypeScript.
export namespace ApiClient {
export class Api {
}
}
Notice that the keyword export was added for both the namespace and the class. The export keyword allows use of the ApiClient namespace and its exported content externally in other files. Here’s an example of using the Api class in Orders class.
import Api = require("./Api");
export namespace OrderService {
import Api1 = Api.ApiClient.Api;
export class Orders {
private readonly api: Api1;
}
}
Here the first Api is the name of the file. ApiClient is the namespace and inside that namespace is, again, the Api class. You would import that class from the Api.ts file and assign it to a variable, which in the above case is assigned to Api1. Here’s a screenshot of the sample application’s structure.
About the Author
You May Also Like