TypeScript is a dynamic, open-sourced programming language that manages and maintains large codebases, which is why it’s loved by most developers. If you are familiar with TypeScript (TS), you know that it was designed for both the development of large applications and transcompiling to JavaScript. However, there’s more to TypeScript than these common uses.
At CQL, our team of talented software developers are continuously adapting to and implementing new uses within the TS code base. In this article, we’ll review Abstract Classes, one of these new classes within TypeScript.
Simply put, abstract classes in TS are a way to share functionality and an object contract (like an interface) at the same time. They cannot be instantiated on their own, and are mainly a way to set up a strong inheritance between classes. It’s because of this that writing extensible code in TS can actually be fun.
Cool, But What Does “Abstract” Mean in TypeScript?
Before diving into potential use cases, it’s important to understand what all this means.
Using the “abstract” keyword when declaring a class sets up a couple of things: first, as stated earlier, it means that the class itself can’t be instantiated on its own – in order to use it, it has to be extended by another class.
Second, it allows the use of the “abstract” keyword on properties and methods inside of the class. When another class extends the abstract class in TS, any properties or methods declared as abstract in the parent class must be implemented. TS won’t compile if an abstract property or method isn’t implemented in a child class.
Another great keyword in abstract classes is “readonly,” which this article will also cover.
A Use Case for Abstract Classes Within TypeScript
Recently, I was tasked with implementing a brand loyalty API service in TS. The idea was simple enough: a controller can call the new service, and then the service would know which API to use when returning a standardized object. Here’s where abstract classes really helped.
For instance, every request to the new API needed two things that were the same for every request: an authorization header and a version number. There were also multiple endpoints, so each request had to know what endpoint of the API to hit, and those different endpoints required different payloads. Setting up an abstract class for these things looked something like this:
export abstract class APIRequestDto {
abstract endpoint: string;
abstract requestBody: any;
abstract requestMethod: string;
readonly apiHeader = Bearer ${bearerToken};
readonly version = "v2";
public getUrl = () =>
this.url + this.endpoint + this.urlParams;
public urlParams?: string;
private url = “https://someservice.com/";
}
Let’s break down these three abstract properties – endpoint, requestBody, and requestMethod. Any class that extends APIRequestDto must implement these three properties, or TS won’t compile.
Two readonly properties – authHeader and version. Subclasses of APIRequestDto will have access to these values, but will not be able to change them, since they are marked “readonly.”
One public method, called getUrl, just concatenates our private url property, endpoint, and any optional urlParams.
Setting Up A Request Class for Rewards
A common functionality of loyalty providers would be awarding points for users that make a purchase. Setting up a request class for that specific endpoint would look something like this:
interface IRequestBody {
total: number;
date_purchased: string; // ISO
user_id?: string;
}
export class APIPurchaseRequestDto extends APIRequestDto {
public endpoint = ${this.version}/purchase;
public requestBody: IRequestBody;
public requestMethod = "POST";
constructor({totalGrossPrice, creationDate}: Order, customer?: Customer) {
super();
this.requestBody = {
total: totalGrossPrice.value,
date_purchased: creationDate.toISOString(),
user_id: ${customer?.profile.custom.loyaltyId} || ''
}
}
}
The important part here is that this new class extends our original APIRequestDto. This means that our class must implement, at the very least, the endpoint, requestBody, and requestMethod properties.
An interface was created for the requestBody so nothing would be left out accidentally. This subclass has a constructor, which calls “super()” to set up the base properties, and then create the requestBody object based on some objects that get passed in.
To tie it all together, making a request would look like this:
const callPurchaseEndpoint = async (): Promise => {
const requestObj: APIRequestDto = new APIPurchaseRequestDto(order, customer);
return await fetch(requestObj.getUrl(), {
method: requestObj.requestMethod,
headers: {
Authorization: requestObj.apiHeader,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestObj.requestBody)
}).then(resp => resp.json());
}
Abstract Classes Extensibility Streamlines Requests
Without this new class, all of the properties needed for the request would need to be hardcoded somewhere. By simply creating an instance of APIPurchaseRequestDto, we now know everything we need in order to make the request – the URL we need to hit, the request method, the specific authorization header, and the request body.
This method just calls the endpoint and returns any JSON that comes back. One interesting thing to notice is that the requestObj variable is typed as the parent class. This is important because when it comes time to hit other endpoints, we have the opportunity to abstract the fetch call even further.
All of the properties used in the fetch call exist on the parent class, so it doesn’t care if it’s a purchase endpoint, or perhaps an endpoint to create a user.
That’s where the extensibility really shines – for any new endpoint to hit, we can just create another class that extends our abstract class, implement all the things we need to, and run that through a fetch.
More About Abstract Classes and Classes in TypeScript
Want to know more about abstract classes in TS, or have questions about properly using similar methods? Contact us today and one of our talented Software Developers will be in touch.