Navigating API data conversion on the frontend using Object-Oriented Programming

Currently, I am facing a challenge while developing the frontend of a web application using TypeScript. The dilemma revolves around efficiently converting a data object from an API response into a format suitable for the application.

Let's consider retrieving a BlogpostAPI object from the API:

interface BlogpostAPI {
  title: string;
  content: string;
  createdAt: string;
  // ...and other properties
}

The objective is to transform it into:

interface BlogpostApp {
  title: string;
  content: BlogpostBlock[];
  createdAt: Date;
  // ...along with other properties that might require transformation
}

type BlogpostBlock = {
  role: 'body' | 'header';
  text: string;
}

I am striving for clean code and wish to encapsulate the transformation logic within a class. However, my current solutions seem complex and cumbersome.

For instance, let's say I intend to create a Blogpost class specifically for handling the transformation logic and potential additional functionalities in the future.

class Blogpost implements BlogpostApp {
  title: string;
  content: BlogpostBlock[];
  createdAt: Date;

  constructor(post: BlogpostApp) {
    this.title = post.title;
    this.content = post.content;
    this.createdAt = post.createdAt;
  }

  static fromBlogpostAPI(post: BlogpostAPI): Blogpost {
    const content: BlogpostBlock[] = post.content.split('\n').map((s, idx) => {
      if (idx === 0) return { text: s, role: 'header' };
      return { text: s, role: 'body' };
    };
    const createdAt: Date = new Date(post.createdAt);

    return new Blogpost({ title: post.title, content, createdAt });
  }
}

However, there are some concerns raised by this approach.

  1. The BlogpostApp interface serves solely to enhance the readability of the arguments in the Blogpost constructor.
  2. If the class primarily focuses on transforming the BlogpostAPI interface, having two identical entities (BlogpostApp and Blogpost) with matching properties may lead to confusion.

An alternative could involve creating a separate transformer class without implementing interfaces:

class BlogpostTransformer {
  constructor() {}

  static transform(post: BlogpostAPI): BlogpostApp {
    const content: BlogpostBlock[] = post.content.split('\n').map((s, idx) => {
      if (idx === 0) return { text: s, role: 'header' };
      return { text: s, role: 'body' };
    });
    const createdAt: Date = new Date(post.createdAt);

    return { title: post.title, content, createdAt } as BlogpostApp;
  }
}

Although this method isolates the transformation process, having multiple classes for various data objects obtained from APIs raises concerns about architectural robustness.

It appears that I am missing a conventional approach for performing these transformations. Any assistance would be greatly appreciated!

Answer №1

If you want to transform data in TypeScript, you can utilize the decorators @Transform and @Type, along with the function plainToClass from the package called class-transformer.

import 'reflect-metadata';
import { Type, Transform, plainToClass } from 'class-transformer';

interface BlogpostAPI {
  title: string;
  content: string;
  createdAt: string;
}

class BlogpostApp {
  title: string;
  @Transform(({ value }) => {
    return value.split('\n').map((s, idx) => {
      if (idx === 0)
        return plainToClass(BlogpostBlock, { text: s, role: 'header' });
      return plainToClass(BlogpostBlock, { text: s, role: 'body' });
    });
  })
  content: BlogpostBlock[];

  @Type(() => Date)
  createdAt: Date;
}

class BlogpostBlock {
  role: 'body' | 'header';
  text: string;
}

const blogpostAPIDTO: BlogpostAPI = {
  title: 'test title',
  content: 'test header\ntest body',
  createdAt: new Date().toString(),
};

const blogpostApp = plainToClass(BlogpostApp, blogpostAPIDTO);

console.log(blogpostApp);
console.log(blogpostApp instanceof BlogpostApp); // true
console.log(blogpostApp.content[0] instanceof BlogpostBlock); // true
console.log(blogpostApp.content[1] instanceof BlogpostBlock); // true
console.log(blogpostApp.createdAt instanceof Date); // true

Check it out on stackblitz

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

React Component not displaying properly when used inside a map iteration

I am currently working on rendering multiple components using the .map method on an array with specific content. Although I can't find any errors in the console, the component is not appearing in the DOM as expected. I attempted to set subHeader to nu ...

TS will not display an error when the payload is of type Partial

Why doesn't TypeScript throw an error when making the payload Partial? It seems to only check the first value but not the second one. type UserState = { user: User | null; loading: boolean; error: Error | null } type UserAction = { type: type ...

Drawing a real-time curve using Phaser 3

After reading the article at the following link, I am attempting to create a dynamic curve showing where a bullet intersects with the land in my game before firing. Any suggestions or ideas on how to achieve this would be greatly appreciated. Thank you. L ...

Leverage TypeScript for modifying local node package alterations

As a newcomer to npm and TypeScript, I may be overlooking something obvious. Our team has developed a node package in TypeScript for internal use, resulting in the following file structure: src/myModule.ts myModule.ts The contents of myModule.ts are as f ...

The close button in Angular 4 is unresponsive until the data finishes loading in the pop-up or table

Having trouble with the Close button in Angular 4 popup/table The Pop Up is not closing after clicking anywhere on the screen. I have added backdrop functionality so that the pop-up closes only when the user clicks on the close icon. However, the close i ...

When 'Interval.after' is invoked within the library, Luxon throws an error message stating "Invalid Interval."

Encountering a strange issue with Luxon when the Interval.after method is invoked within the library. const interval = Interval.after(dateTime, duration); The following log pertains to the application DateTime__Duration, with the second line representing ...

Next.js 13 app directory experiences 404 Not Found error due to dynamic routing issues

I recently built a straightforward to-do app using Next.js 13 paired with TypeScript. The process involved creating an array of objects, each comprising an id string and a name string. Subsequently, I iterated through the list and showcased the names withi ...

Delete an entry in a singular mapping in a one-to-one connection [TypeORM]

Is there a way to remove an index from a one-to-one relationship in TypeORM? @OneToOne(() => Customer, { cascade: true }) @JoinColumn({ name: 'customer', referencedColumnName: 'uid' }) customer: Customer I searched the d ...

Interfaces and Accessor Methods

Here is my code snippet: interface ICar { brand():string; brand(brand:string):void; } class Car implements ICar { private _brand: string; get brand():string { return this._brand; } set brand(brand:string) { this. ...

Getting Session from Next-Auth in API Route: A Step-by-Step Guide

When printing my session from Next Auth in a component like this, I can easily see all of its data. const session = useSession(); // ... <p>{JSON.stringify(session)}</p> I am facing an issue where I need to access the content of the session i ...

Accessing the various types within a monorepo from a sibling directory located below the root folder

Seeking assistance in resolving a referencing types issue within a TypeScript monorepo project. Unsure if it is feasible given the current setup. The project structure is as follows: . ├── tsconfig.json ├── lib/ │ └── workers/ │ ...

Using Arrow Functions in Angular 2 Typescript with Support for IE11

Are arrow functions in Typescript for Angular2 compatible with IE 11? I have come across information stating that arrow functions in javascript may not be supported in IE 11, but I am uncertain if the same applies to Typescript. ...

The number entered will be incorporated into the API URL key value by passing the variable from page.html to services.ts

Recently diving into the world of Ionic, Angular, and Typescript, I've had a burning question. Can the number inputted be added to the API URL as one of the key values? I came across this helpful guide, specifically focusing on key event filtering (wi ...

Modifying the value upon saving in Adonis JS model

Using Adonis js I am facing an issue when trying to convert an ISO string to Datetime while saving data (the opposite of serializing DateTime fields to ISO string). I cannot find a way to do this in the model, like I would with a mutator in Laravel. Whene ...

A guide to incorporating Material-UI ThemeProvider and WithStyles with Typescript

I've been feeling frustrated lately as I've been dedicating the past few days to migrating my React application from JavaScript to TSX. While I appreciate the type checking that TSX provides, I'm struggling with understanding how to implemen ...

What causes the object type to shift away from 'subscribe'?

Currently, I am facing an issue with retrieving a Coupon object from the server using a REST API in combination with Angular. The problem arises when I attempt to access the 'subscribe' method - within the 'subscribe', the object is of ...

Is Python capable of passing object references?

I have been studying Python after working with dotnet, and am currently in the process of developing an application that interacts with a webservice. The webservice is structured in a flat manner, with multiple calls related to sessions (such as logging o ...

How to simulate a typescript class using vitest

I am encountering a situation where I have a class A that imports another class B from a separate module and creates an instance of it. While writing tests for class A, I want to stub or mock some of the methods of class B. Below is an example code snippe ...

Merging arrays with the power of ES6 spread operator in Typescript

My goal is to merge two arrays into one using the spread object method as shown in the code snippet below: const queryVariable = { ...this.state, filters: [...Object.keys(extraFilters || {}), ...this.state.filters], } The this.state.filte ...

The element is assumed to have an 'any' type due to the index expression not being of type 'number'. - Error in Index Signature

I've recently started using TypeScript and encountered the following issue: Element implicitly has an 'any' type because index expression is not of type 'number' This error message appears on this line --> const { status, msg ...