Exploring the world of third-party APIs

I am currently working on fetching data from an external API and displaying it.

In order to enhance flexibility, I am aiming to completely separate the API integration from my code and use custom-defined data structures instead.

Here is a brief visual overview: https://i.stack.imgur.com/cxjaU.png

To illustrate this concept further, let's consider an example:

Imagine we are dealing with data related to people:

While API v1.0 returns {"name": "John"}, API v1.1 might return {"pName": "John"}.

To avoid potential issues caused by minor changes in the API response, I plan to internally define two interfaces: one for parsing the API response and another as a structure for the data itself:

interface IPersonDataStructure {
    name : string;
}

interface IPersonDataParser {
    parse(input: string) : IPersonDataStructure;
}

My goal is to create a class that combines the parser and the data structure:

// This class utilizes any parser that implements IPersonDataParser
// And uses IPersonDataStructure 
class Person {

}

However, I have hit a roadblock at this stage! I am unsure about how to effectively merge the two elements together!

I am not keen on having an instance per Person's instance:

let aPerson = new Person(new Parser(data))

As the parser should ideally be stateless (similar to a function).

The challenge lies in TypeScript restrictions which make it difficult to achieve this with classes:

class Parser implements IPersonDataParser {
    static public function parse(data : string) : IPersonDataStructure {
        return {...}
    }
}

class Person {
    private _data : IPersonDataStructure;

    constructor(data : string, parser : IPersonDataParser) {
        this._data = parser.parse(data)
    }
}

Callbacks could be a viable solution, but only if I can verify their signature.

For instance, the following does not validate correctly:

type PersonDataParser = (data : string) => IPersonDataStructure;

// Oops.. argument is missing!
let aParser = () => {
    return {...}
}

let aPerson = new Person('data', aParser)

Are there any suggestions on how to overcome this challenge?

Answer №1

One way to achieve this is by implementing a static method that satisfies the interface requirements using type inference and structural types. Here's an example:

interface IPersonDataStructure {
    name : string;
}

interface IPersonDataParser {
    parse(input: string) : IPersonDataStructure;
}

class Parser {
    public static parse(data : string) : IPersonDataStructure {
        return { name: 'Steve' };
    }
}

class Person {
    private _data : IPersonDataStructure;

    constructor(data : string, parser : IPersonDataParser) {
        this._data = parser.parse(data)
    }
}

let person = new Person('', Parser);

Alternatively, you could simplify the design of the Person class so that it only represents a person without needing a mapper during construction. This can be achieved as follows:

interface IPersonDataStructure {
    name : string;
}

class Person {
    constructor(private data : IPersonDataStructure) {
    }
}

class PersonMapper {
    public static map(data: string): Person {
        return new Person({
            name: 'Steve'
        });
    }
}

let person = PersonMapper.map('...');

If your data includes a version number, you can use that information to determine the appropriate mapping.

Answer №2

Have you considered implementing an adapter to determine which property was returned by the API?

interface ApiResponse {
    firstName?: string;
    lastName?: string;
}

class Person {
    public name: string;

    constructor (name: string) {
        this.name = name;
    }
}

class ApiResponseAdapter {
    private getName(response: ApiResponse): string {
        if (response.firstName) return firstName;
        if (response.lastName) return lastName;

        // if neither are set, return null
        return null;
    }

    public adapt(response: ApiResponse): Person {
        let name = this.getName(response);

        if (name === null) {
            throw new Error("Invalid name for response: " + JSON.stringify(response));
        }

        return new Person(name);
    }
}

Another approach is to define a base ApiResponse interface with different implementations for handling the behavior:

interface ApiResponse {
    fullName: string;
}

class Api_V1_0_Response implements ApiResponse {
    public fullName: string;

    constructor (json: any) {
        this.fullName = json["fullName"];
    }
}

class Api_V1_1_Response implements ApiResponse {
    public fullName: string;

    constructor (json: any) {
        this.fullName = json["lastName"];
    }
}

class Person {
    public name: string;

    constructor (name: string) {
        this.name = name;
    }
}

class ApiResponseAdapter {
    public adapt(response: ApiResponse): Person {
        return new Person(
            response.fullName
        );
    }
}

Or take it a step further by creating an abstract BaseApiResponse class that is extended by specific response classes:

interface ApiResponse {
    fullName: string;
}

abstract class BaseApiResponse implements ApiResponse {
    public fullName: string;

    constructor (nameKey: string, json: any) {
        this.fullName = json[nameKey];
    }
}

class Api_V1_0_Response extends BaseApiResponse {   
    constructor (json: any) {
        super("fullName", json);
    }
}

class Api_V1_1_Response extends BaseApiResponse {   
    constructor (json: any) {
        super("lastName", json);
    }
}

class Person {
    public name: string;

    constructor (name: string) {
        this.name = name;
    }
}

class ApiResponseAdapter {
    public adapt(response: ApiResponse): Person {
        return new Person(
            response.fullName
        );
    }
}

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

Implementing the breadcrumb component within dynamically loaded modules loaded through the router-outlet component

I'm currently working on an angular 8 breadcrumb component for my application. The requirement is to display it in the content of the page, not just in the header, and it should never be located outside the router-outlet. This has posed a challenge fo ...

The Validator in Angular Formbuilder must have a specific character requirement

Can someone help me with a regex validator pattern in Angular Formbuilder to ensure that the field CityStateZip contains at least one comma as a special character? this.editAddressForm = this.formBuilder.group({ 'CustomerName': [null, ...

Are fp-ts and Jest the perfect pairing for testing Option and Either types with ease?

When working with fp-ts, and conducting unit tests using Jest, I often come across scenarios where I need to test nullable results, typically represented by Option or Either (usually in array find operations). What is the most efficient way to ensure that ...

"Troubleshooting Typecscript and Angular: Dealing with mismatched argument

How can I resolve this Angular error: (response: HttpResponse<User>) => { which results in the following error message: Argument of type '(response: HttpResponse<User>) => void' is not assignable to parameter of type '(val ...

Can I retrieve the return type of useFetch in Nuxt3?

I am running into an issue while trying to specify the APIBody type in the following manner: Property 'test' does not exist on type 'NonNullable<PickFrom<_ResT, KeysOf>>'. It seems like there is no straightforward way to def ...

Can you explain how this promise functions within the context of the mutation observer, even without an argument?

Recently, I came across a mutation observer in some TypeScript code that has left me puzzled. This particular implementation of a promise within the mutation observer seems unconventional to me: const observer = new MutationObserver((mutations: MutationR ...

The functionality of MaterializeCSS modals seems to be experiencing issues within an Angular2 (webpack) application

My goal is to display modals with a modal-trigger without it automatically popping up during application initialization. However, every time I start my application, the modal pops up instantly. Below is the code snippet from my component .ts file: import ...

typescript - instantiate an object using values stored in an array

Assume we have a model defined as follows. export interface Basicdata { materialnumber: number; type: string; materialclass: string; } We also have an array containing values that correspond directly to the Basicdata model in order, like this: ...

Stop the controller from reloading when navigating in Angular2/Ionic2

Initially, I developed my app using tabs. When navigating to a page, the view would load for the first time (fetch data from API and display it), and upon returning to the same page, nothing would reload because the controller did not run again. Recently, ...

Tips for adjusting HighCharts layout with highcharts-vue integrations

I have a fairly simple component: <template> <div> <chart v-if="!loading" ref="priceGraph" constructor-type="stockChart" :options="chartData" ...

After the form submission, my Next.js component keeps rendering repeatedly in a cumulative manner

I am currently working on a memory game application using Next.js, Node.js, and Express.js. I seem to be encountering an issue specifically with the login page. Initially, there are no issues when submitting the form for the first time. However, after the ...

Tips for integrating the react-financial-charts library into your React and JavaScript project

While exploring the react-financial-charts library, I discovered that it is written in TypeScript (TS). Despite my lack of expertise in TypeScript, I am interested in using this library in my React+JS project due to its active contributions. However, I hav ...

What techniques can I use to modify an object while it's being iterated through?

I've been attempting to manipulate the object while looping through it, but unfortunately, it's not working. How can I modify it so that the constant patient includes the property lastActivity inside the this.model array? My code looks like this ...

Error encountered when attempting to retrieve token from firebase for messaging

I am currently working on implementing web push notifications using Firebase. Unfortunately, when attempting to access messaging.getToken(), I encounter an error stating "messaging is undefined." Below is the code snippet I am utilizing: private messaging ...

Issue with extending mongoose.Document properly in NodeJS and TypeScript using a custom interface with mongoose

I recently started learning Typescript and tried to follow this guide to help me along: After following the guide, I implemented the relevant code snippets as shown below: import { Document } from "mongoose"; import { IUser } from "../interfaces/user"; ...

Generate sample data within a fixture

Currently, I am in the process of working on a project that involves creating users and conducting tests on those users. To generate user data such as first name and last name, I am utilizing the faker tool. My goal is to create a user with these generated ...

All authentication logic in Angular encapsulated within the service

I am considering moving all the business logic into the auth service and simply calling the method on the component side. Since none of my functions return anything, I wonder if it's okay or if they will hang. COMPONENT credentials: Credentials = ...

Issue with TypeScript Declaration File in NPM module functionality

Recently, I've been working on developing a package for NPM. It's essentially a JSON wrapped database concept, and it has been quite an enjoyable project so far. However, I've been facing some challenges when trying to include declarations f ...

Retrieving Headers from a POST Response

Currently, I am utilizing http.post to make a call to a .NET Core Web API. One issue I am facing is the need to extract a specific header value from the HTTP response object - specifically, the bearer token. Is there a method that allows me to achieve thi ...

Utilizing flatMap to implement nested service calls with parameters

Recently, I encountered an issue while working on a service call to retrieve data from a JSON file containing multiple items. After fetching all the items, I needed to make another service call to retrieve the contents of each item. I tried using flatMap f ...