DEV Community

Cover image for Interface Inheritance, Types and Enums with TypeScript - Practical Example
Diego Sevilla
Diego Sevilla

Posted on

Interface Inheritance, Types and Enums with TypeScript - Practical Example

On our day to day as software engineers it’s important to take into consideration the fact that how we manage the incoming DTOs (Data Transfer Objects) to a system is very important in terms of code scalability, better structuring/organization and other aspects related to the long term maintenance of an application.

In this occasion, I will be sharing one story of interfaces management in TypeScript and how you can experiment and get the most out of them with a practical basic example 🍃


Table of Contents

📌Introduction
📌Analysis

📍Improvements
📍Enums Usage
📍Creating a Report

📌Solution Code
📌Conclusions


Introduction

Not long ago, I worked for a company that managed a huge amount of transfers and had a fairly big history of data created over time. While working on a migration from a desktop app 💻 to a web app ☁️, the necessity of a page in charge of the management of reports came up and I got to work.

The creation of reports required the sending of information to an API via POST HTTP requests, which had data variations with each kind of report. Upon closer analysis I realized there were, indeed, specific fields shared between the creation of the report documents.

Here is a simplified example:

  const cashierReportData = {
    idAgent: 999,
    reportName: 'cashier-report',
    fileFormat: 'pdf',
    dateFrom: '2019-09-01',
    dateTo: '2019-12-01',
    idCashier: 44893,
    factor555: '0.236738938',
    reportCode: 123
  }

  const claimReportData = {
    idAgent: 777,
    reportName: 'claim-report',
    fileFormat: 'excel',
    language: 'EN-US',
    showRate: true,
    dateFrom: '2021-03-14',
    dateTo: '2022-11-07',
    reportCode: 456
  }

  const exchangeReportData = {
    idAgent: 555,
    reportName: 'exchange-report',
    fileFormat: 'word',
    imageQR: 'http://dom.io/img.jpg',
    quanatityRate: 0.44,
    dateFrom: '2017-05-04',
    dateTo: '2019-05-04',
    reportCode: 789
  }
Enter fullscreen mode Exit fullscreen mode

At first glance, what shared fields do you see on the objects? 🔍 Humm… I bet you found idAgent, reportName, dateFrom, dateTo and others.

These objects might seem a little messi tough 🤯 and would become hard to manage if the number of fields increases, as well as the number of report kinds and so on. Thinking about scalability, it’s important to take a time as dedicated developers and make sure we know what type of data we are handling.


Ha! What would a TypeScript developer had done in that case? 🚀 ️Well of course, the solution was to leverage the power of interface inheritance. This approach would allow us to validate the information integrity and its types when generating a specific report before sending the data to the server.

Analysis

Because I was managing POST HTTP requests with query parameters and different body requests these are the interfaces I created:

export interface CreateReportQueryParams {
  idAgent: number;
  reportName: string;
  fileFormat: string;
}
interface CreateReportGeneralRequest {
  dateFrom: Date;
  dateTo: Date;
  reportCode: number;
}
export interface CreateReportCashier extends CreateReportGeneralRequest {
  idCashier: number;
  factor555: boolean;
}
export interface CreateReportClaim extends CreateReportGeneralRequest {
  language: string;
  showRate: boolean;
}
export interface CreateReportExchange extends CreateReportGeneralRequest {
  imageQR: string;
  quantityRate: number;
}

export type CreateReportRequestBodyTypes =
  | CreateReportCashier
  | CreateReportClaim
  | CreateReportExchange;
Enter fullscreen mode Exit fullscreen mode

So what is happening here? After breaking down all the fields, now we have 4 concepts:

  1. An interface for the query parameters
  2. A parent interface which has the request body shared fields
  3. Specific interfaces for each kind of report which extend the parent interface
  4. A discriminating union type that joins the specific interfaces

Improvements

So we are good, but wait… Did you notice something that can be improved? 🤔 These interfaces will help us by making sure if we want to get, I can say, the exchange report; we will need to adhere to its data type.

That’s exacly what we are looking for, nevertheless what would happen if someone misstypes reportName or fileFormat or reportCode and enters a wrong value? We would probably get a 404 — Bad Request response from the server or something similar, I most say. Well, in that case 💡we can take advantage of Enums.

Enums Usage

Enums allows us to define a set of named constants and thus a set of distinct cases:

export enum FileFormats {
  pdf = "pdf",
  word = "word",
  excel = "excel",
}
export enum ReportNames {
  ExchangeReport = "exchange-report",
  ClaimReport = "claim-report",
  CashierReport = "cashier-report",
}
export enum ReportCodes {
  ExchangeCode = 123,
  ClaimCode = 456,
  CashierCode = 789,
}

export interface CreateReportQueryParams {
  idAgent: number;
  reportName: ReportNames;
  fileFormat: FileFormats;
}
interface CreateReportGeneralRequest {
  dateFrom: Date;
  dateTo: Date;
  reportCode: ReportCodes;
}
Enter fullscreen mode Exit fullscreen mode

With Enums no wrong values are allowed and therefore possible typing issues are prevented.

Using axios, the http service would look similar to this:

export interface CreateReportResponse {
  reportUrl: string;
}

//AxiosResponse schema here: https://axios-http.com/docs/res_schema

export class CreateReportService {
  /**
   * Will create a report.
   * ---
   * @param {CreateReportQueryParams} params request query parameters
   * @param {CreateReportRequestBodyTypes} requestBody the request body object which can have different types
   * @returns {CreateReportParams} The generated url for the report
   */
  createReport = async (
    params: CreateReportQueryParams,
    requestBody: CreateReportRequestBodyTypes
  ): Promise<AxiosResponse<CreateReportResponse>> => {
    const reponse = 
      await axios.post("http://dom.io/api/reports/create", 
        requestBody, 
        {
          params: {
            ...params,
          }
        }
      );
    return reponse;
  };
}
Enter fullscreen mode Exit fullscreen mode

It’s so nice when you have a well typed function, isn’t it? 😏💻

Note: It’s worth saying we have an interfaceCreateReportResponse, that defines the data response structure we are expecting.

Creating a Report

So now that we have our service class, interfaces, types and enums… let’s create an exchange report:

  const exchangeReportParams: CreateReportQueryParams = {
    idAgent: 4444,
    reportName: ReportNames.ExchangeReport,
    fileFormat: FileFormatType.pdf,
  };
  const exchangeReportRequest: CreateReportExchange = {
    imageQR: "http://domain.com/image.jpg",
    quantityRate: 0.44,
    dateFrom: new Date("2017-05-04"),
    dateTo: new Date("2019-05-04"),
    reportCode: ReportCodes.ExchangeCode,
  };
  const createReportService = new CreateReportService();
  const exchangeReportResponse = await createReportService.createReport(
    exchangeReportParams,
    exchangeReportRequest
  );
Enter fullscreen mode Exit fullscreen mode

After some analysis, the structure we are using to create new reports is now quite more neat and organized than before. Beautiful! 🌹💯


Solution Code

Lastly, putting all together, this was the solution that would allow everyone know the exact types of information required for the generation of new reports:

export enum FileFormats {
  pdf = "pdf",
  word = "word",
  excel = "excel",
}
export enum ReportNames {
  ExchangeReport = "exchange-report",
  ClaimReport = "claim-report",
  CashierReport = "cashier-report",
}
export enum ReportCodes {
  ExchangeCode = 123,
  ClaimCode = 456,
  CashierCode = 789,
}

export interface CreateReportResponse {
  reportUrl: string;
}
export interface CreateReportQueryParams {
  idAgent: number;
  reportName: ReportNames;
  fileFormat: FileFormats;
}
export interface CreateReportGeneralRequest {
  dateFrom: Date;
  dateTo: Date;
  reportCode: ReportCodes;
}
export interface CreateReportCashier extends CreateReportGeneralRequest {
  idCashier: number;
  factor555: boolean;
}
export interface CreateReportClaim extends CreateReportGeneralRequest {
  language: string;
  showRate: boolean;
}
export interface CreateReportExchange extends CreateReportGeneralRequest {
  imageQR: string;
  quantityRate: number;
}

export type CreateReportRequestBodyTypes =
  | CreateReportCashier
  | CreateReportClaim
  | CreateReportExchange;

export class CreateReportService {
  createReport = async (
    params: CreateReportQueryParams,
    requestBody: CreateReportRequestBodyTypes
  ): Promise<AxiosResponse<CreateReportResponse>> => {
    const reponse = 
      await axios.post("http://dom.io/api/reports/create", 
        requestBody, 
        {
          params: {
            ...params,
          }
        }
      );
    return reponse;
  };
}
Enter fullscreen mode Exit fullscreen mode

Conclusions

  1. The usage of interfaces, types and enums can help us represent concrete objects, define the structure of objects and have a strict typed system that will be less prompt to typing errors.
  2. Enums allows us to define a set of named constants and thus a set of distinct cases.
  3. Adhering to the type of data that is received/sent on a system is easier when leveraging TypeScript powers and thinking of clean code.

Well that was quite a lot of concepts! Always remember that the best way to keep the concepts fresh is to practice. The TypeScript Handbook will always be your source of truth when discovering new fun stuff with TypeScript, so don’t forget to give it a look. 😃

Here are some cool resources you can use to get more into TypeScript:

Utility Types | Decorators | Namespaces

Happy Coding! 🤓💻

Top comments (0)