Changing data types conditionally in Angular using TypeScript

My angular component relies on the API Response for its functionality. I receive these responses from an external API.

  1. When there are no errors

    Data : { Status: string; Data: number; }

  2. When an error occurs, this is what I get.

    Data : { Status: string; Error: string; Message: string; }

Since 'Status' is a common property, I have attempted to unify the response structures using interfaces.

interface ApiModel {
  Status: string;
}

interface DataModel extends ApiModel {
  Data: number;
}
interface ErrorModel extends ApiModel {
  ErrorCode: string;
  Message: string;
}

I am utilizing TypeScript to define my variable with multiple possible types and pass that type in the response.

I prefer not to use optional types to avoid potential issues.

What I aim for is,

if (typeof Response === DataModel){
Response: DataModel;
// Other code
}
else {
Response: ErrorModel;
// Other code
}

Here is how the API call is structured:

getReminder(reminderId: number) {
    const API_URL = `${this.BASE_URL}users/${this.userId}/reminders/${reminderId}`;
    return this.httpClient.get<ErrorModel | DataModel>(API_URL, this.options).pipe(
      map((data: ErrorModel | DataModel) => {
        return data;
      })
    );
  }

Answer №1

From my perspective, your model resembles a concept known as Discriminated union

If you structure it accurately, the compiler will be capable of narrowing down without the need for additional code. Take into account:

  • There is no requirement to cast downwards after confirming type predicate
  • The discriminator checks either of the two union members - if it is not DataModel, the compiler can infer that it is an ErrorModel
interface DataModel {
  Status: 'success';
  Data: number;
}

interface ErrorModel {
  Status: 'error'
  ErrorCode: string;
  Message: string;
}

type ApiModel = DataModel | ErrorModel;

function foo(responseData: ApiModel): void {
  if (responseData.Status === 'success') {
    console.log(responseData.Data);
  } else {
    console.log(responseData.ErrorCode);
  }
}

foo({Status: 'success', Data: 10});

If your data cannot be represented as a discriminated union, you can still model the response as a union, but you will have to utilize a [Type predicate] to determine which member of the union you are working with

interface ApiModelBase {
  Status: string;
}

interface DataModel extends ApiModelBase {
  Data: number;
}
interface ErrorModel extends ApiModelBase {
  ErrorCode: string;
  Message: string;
}

type ApiModel = DataModel | ErrorModel;


function isDataModel(response: ApiModel): response is DataModel {
  return (response as DataModel).Data !== undefined;
}

function foo(responseData: ApiModel): void {
  if (isDataModel(responseData)) {
    console.log(responseData.Data);
  } else {
    console.log(responseData.ErrorCode);
  }
}

foo({Status: 's1', Data: 10});

TS Playground

Answer №2

After reviewing all the answers above, I have concluded that it is possible to conditionally modify the interface in TypeScript but not in Angular components. Therefore,

interface ApiModel {
Status: string;
Data?: string;
ErrorCode?: string;
Message?: string;
}

While this may appear more cost-effective or simpler, my aim was to achieve this.

Alternatively, utilizing a Class also achieves the desired outcome. With a class, I can define conditional types as there is logic that can be implemented. However, if we opt for an interface, the solution provided above is much more straightforward and user-friendly.

Answer №3

A method to determine the type of data being received is by using this particular approach, which involves inline type conversion:

function checkDataModel(response: ApiModel): response is DataModel {
  return (response as DataModel).Data !== undefined;
}

// Assuming `responseData` holds the API response
const responseData: ApiModel = ...;

if (checkDataModel(responseData)) {
  // The data belongs to DataModel type
  const dataResponse: DataModel = responseData;
  // Additional operations for DataModel
} else {
  // The data corresponds to ErrorModel type
  const errorResponse: ErrorModel = responseData;
  // Additional operations for ErrorModel
}

Answer №4

Have you considered utilizing Union types in TypeScript to accomplish your goal?

type UserResponse = DataModel | ErrorModel;

Here is an example of how it could be used:

const userResponse: UserResponse = {
  Status: "Failure",
  Message: "Error message here"
};

if (userResponse instanceof DataModel) {
  This is a DataModel instance.
} else if (userResponse instanceof ErrorModel) {
  This is an ErrorModel instance.
}

Answer №5

import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

isSuccessResponse(response: ApiResponse): response is DataModel {
  return response.Status === 'success';
}

fetchReminder(reminderId: number): Observable<DataModel | ErrorModel> {
  const API_URL = `${this.BASE_URL}users/${this.userId}/reminders/${reminderId}`;
  return this.httpClient.get<ApiResponse>(API_URL, this.options).pipe(
    map((response: ApiResponse) => {
      if (isSuccessResponse(response)) {
        const responseData: DataModel = response as DataModel;
        return responseData;
      } else {
        // Handling error response
        const errorResponse: ErrorModel = response as ErrorModel;
        throw errorResponse;
      }
    })
  );
}

Interfaces remain the same as before

interface ApiResponse {
  Status: string;
}

interface DataModel extends ApiResponse {
  Data: number;
}

interface ErrorModel extends ApiResponse {
  ErrorCode: string;
  Message: string;
}

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

Showing canvas lines while dragging (using only plain JavaScript, with React.JS if needed)

Is there a way to add lines with two clicks and have them visible while moving the mouse? The line should only be drawn when clicking the left mouse button. Any suggestions on how I can modify my code to achieve this functionality? Currently, the lines are ...

How to store an imported JSON file in a variable using TypeScript

I am facing a challenge with a JSON file that stores crucial data in the following format { "login": { "email": "Email", "firstName": "First name", "lastName": "Last name", ...

explore and view all images within a directory

Hello, I am new to NextJS and encountering some issues. I have a folder in my public directory containing 30 images that I want to import all at once in the simplest way possible, without having to import each image individually by name. Here is the curren ...

In Next.js, the Typescript compiler does not halt when an error occurs

I am looking to incorporate Next.js with TypeScript into my project. I followed the official method of adding TypeScript to Next.js using npx create-next-app --typescript. Everything seemed fine, but when a TypeScript error occurs (e.g. const st: string = ...

Is it possible to include a conditional type in TypeScript using inlining?

Apologies if this question seems basic to those well-versed in typesystems. I'm puzzled by the difference in outcomes when inlining a conditional statement in TypeScript. For instance, using the Extract conditional leads to different results dependin ...

Encountering an issue while attempting to input a URL into the Iframe Src in Angular 2

When I click to dynamically add a URL into an iframe src, I encounter the following error message: Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'SafeValue%20must%20use%20%5Bproperty%5D' To ensure the safety of the ...

I am experiencing an issue with React Select where it does not seem to recognize the value I have

Forgive me if this question sounds naive, I am still relatively new to Reactjs, I kindly ask not to suggest using Hooks as they are not compatible with my current project. Currently, I am focusing on a form and its validation process. Here is the snippe ...

VSCode still throwing a replaceAll warning, despite targeting ES2021

Here is the tsconfig file for my Vue project: { "extends": "@vue/tsconfig/tsconfig.web.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/**/*.json"], "exclude ...

Typescript with Angular: Despite having 7 values in the map, Map.get is returning undefined

Why does Map.get always return undefined when using a number from a form element (extra1) in this code snippet? extraById = new Map<number,Extra>(); @Input() extra1: number = -1; formChanged(carConfigurationFormChanged : any) { const index ...

What is the best way to generate a dummy ExecutionContext for testing the CanActivate function in unit testing?

In my authGuard service, I have a canActivate function with the following signature: export interface ExecutionContext extends ArgumentsHost { /** * Returns the *type* of the controller class which the current handler belongs to. */ get ...

Having difficulty passing a function as a parameter from a NextJS component

I have a code snippet like this in a NextJS component: const [currentGPS, setCurrentGPS] = useState({coords:{latitude:0.0,longitude:0.0}}) useEffect(() => { utl.getGPSLocation( (v:{coords: {latitude:number; longitude:n ...

Using Ionic 3 to create a list view that includes buttons linked to the items clicked

I need assistance with modifying the button icon in a list layout: <ion-list inset *ngFor="let audio of event.audios; let i = index"> <ion-item> <div class="item-text-center item-text-wrap"> {{audio.fileName}} </div& ...

Input a new function

Trying to properly type this incoming function prop in a React Hook Component. Currently, I have just used any which is not ideal as I am still learning TypeScript: const FeaturedCompanies = (findFeaturedCompanies: any) => { ... } This is the plain fun ...

Store Angular 17 control flow in a variable for easy access and manipulation

Many of us are familiar with the trick of "storing the conditional variable in a variable" using *ngIf="assertType(item) as renamedItem" to assign a type to a variable. This technique has always been quite useful for me, as shown in this example: <ng-t ...

Customizing default attribute prop types of HTML input element in Typescript React

I am currently working on creating a customized "Input" component where I want to extend the default HTML input attributes (props). My goal is to override the default 'size' attribute by utilizing Typescript's Omit within my own interface d ...

A TypeScript example showcasing a nested for-of loop within several other for loops

Is it possible to generate values from an Array of objects in the following way? const arr = [ { type: "color", values: [ { name: "Color", option: "Black", }, { name: "C ...

Leveraging FormControlName in Typescript to Interact with HTML Components in Angular 4

How can I use FormControlName to access HTML elements in typescript? Typically, I am able to access HTML elements using their ID. For example: var element = document.getElementById("txtID") But is it possible to access the element without using its ID a ...

Ways to classify the prop type of a functional component by specifying the different types of props for the FC

I have created two functional components that require different prop types. export interface OrderImageProps { orders: ICartItem[] } const OrderImage: React.FC<OrderImageProps> = (props: OrderImageProps) => { return ( <div> ...

Do you find this unattractive? What are some ways to improve this unsightly JavaScript statement?

This code seems messy, how can I better structure this switch statement? function renderDataTypeIcon(dataType: string) { let iconName; switch (dataType) { case "STRING": //TODO - ENUM iconName = "text"; break; ...

What steps can be taken to resolve the error message "Module '../home/featuredRooms' cannot be found, or its corresponding type declarations"?

Upon deploying my site to Netlify or Vercel, I encountered a strange error. The project runs smoothly on my computer but seems to have issues when deployed. I am using TypeScript with Next.js and even attempted renaming folders to lowercase. Feel free to ...