Guide to making type-safe web service requests using Typescript

When utilizing Angular for web service calls, it's important to note that the type of the returned object is not automatically verified. For example, let's say I have a Typescript class named Course:

export class Course {
  constructor(
    public id: number,
    public name: string,
    public description: string,
    public startDate: Date
  ) {}
}

and a corresponding DataService class:

import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';

@Injectable()
export class DataService {
  constructor(private http: Http) { }

  public get<T>(url: string): Observable<T> {
    const headers = new Headers();
    headers.append('content-type', 'application/json');
    const options = new RequestOptions({ headers: headers, withCredentials: 
true });
    return this.http.get(url, options)
      .map(response => response.json() as T);
  }
}

Now, when I try to retrieve data by calling the DataService in my CourseComponent:

import { Component } from '@angular/core';
import { DataService } from './data.service';
import { Course } from './course';


@Component({
  selector: 'app-course',
  templateUrl: './course.component.html',
  styleUrls: ['./course.component.css']
})
export class CourseComponent {
  private courseId: number;

  constructor(private dataService: DataService) { }

  public getData() {
    this.dataService.get<Course>(`http://myapi/course/${this.courseId}`)
    .subscribe(
      course => this.course = course;
    );
  }
}

In this scenario, no compile errors will be triggered as long as the data service correctly provides an object of type "Course".

However, if the API unexpectedly returns JSON in a different format like this:

{
    "uniqueId": 123,
    "name": "CS 101",
    "summary": "An introduction to Computer Science",
    "beginDate": "2018-04-20"
}

This would not result in a compile time error, but could lead to runtime errors if operations are attempted based on non-existing properties like id, summary, and startDate. This situation compromises some of the type safety that TypeScript offers.

Answer №1

Resolution

To address this issue, we need to make adjustments to our data service:

import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';


@Injectable()
export class DataService {
  constructor(private http: Http) { }

  private validateObjectWithTemplate(template: any,  obj: any, graph: string[]) {
    if (!template) {
      return;
    }

    const graphString = graph.join('.');

    Object
      .getOwnPropertyNames(template)
      .forEach(property => {
        if (!obj.hasOwnProperty(property)) {
          console.error(`Object is missing property: ${graphString}.${property}`);
        } else {
          const newGraph = graph.map(i => i);
          newGraph.push(property);
          this.validateObjectWithTemplate(template[property], obj[property], newGraph);
        }
      });
  }

  public get<T>(url: string, template: T): Observable<T> {
    const headers = new Headers();
    headers.append('content-type', 'application/json');
    const options = new RequestOptions({ headers: headers, withCredentials: true });
    return this.http.get(url, options)
      .map(response => {
        const obj = response.json() as T;
        this.validateObjectWithTemplate(template, obj, []);
        return obj;
      });
  }
}

We also need to introduce a "template" for our Course class:

export class Course {
  public static readonly Template = new Course(-1, '', '', new Date());
  constructor(
    public id: number,
    public name: string,
    public description: string,
    public startDate: Date
  ) {}
}

Additionally, we should update our Course component to provide the template to the data service:

import { Component } from '@angular/core';
import { DataService } from './data.service';
import { Course } from './course';

@Component({
  selector: 'app-course',
  templateUrl: './course.component.html',
  styleUrls: ['./course.component.css']
})
export class CourseComponent {
  private courseId: number;

  constructor(private dataService: DataService) { }

  public fetchData() {
    this.dataService.get<Course>(`http://myapi/course/${this.courseId}`, Course.Template)
    .subscribe(
      course => this.course = course;
    );
  }
}

By following these steps, the data service will validate that the JSON response from the API meets the requirements to be considered a valid Course object.

What about arrays? If our classes include arrays, such as in the case of our Student class:

import { Course } from './course';

export class Student {
  public static readonly Template = new Student(-1, '', [Course.Template]);
  constructor(
    public id: number,
    public name: string,
    public courses: Course[]
  ) {}
}

In this scenario, we must ensure that any arrays in the template have at least one item for verification. The data service needs to be updated accordingly:

import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';


@Injectable()
export class DataService {
  constructor(private http: Http) { }

  private validateObjectWithTemplate(template: any,  obj: any, graph: string[]) {
    if (!template) {
      return;
    }

    const graphString = graph.join('.');

    if (obj === undefined) {
      console.error(`Object is missing property: ${graphString}`);
      return;
    }

    if (obj === null) {
      return;
    }

    if (Array.isArray(template)) {
      if (!template[0]) {
        console.error(`Template array is empty: ${graphString}`);
        return;
      }

      if (!Array.isArray(obj)) {
        console.error(`Object is not an array: ${graphString}`);
        return;
      }

      if (!obj[0]) {
        console.log(`Object array is empty and cannot be verified: ${graphString}`);
        return;
      }

      template = template[0];
      obj = obj[0];
    }

    Object
      .getOwnPropertyNames(template)
      .forEach(property => {
        if (!obj.hasOwnProperty(property)) {
          console.error(`Object is missing property: ${graphString}.${property}`);
        } else {
          const newGraph = graph.map(i => i);
          newGraph.push(property);
          this.validateObjectWithTemplate(template[property], obj[property], newGraph);
        }
      });
  }

  public get<T>(url: string, template: T): Observable<T> {
    const headers = new Headers();
    headers.append('content-type', 'application/json');
    const options = new RequestOptions({ headers: headers, withCredentials: true });
    return this.http.get(url, options)
      .map(response => {
        const obj = response.json() as T;
        this.validateObjectWithTemplate(template, obj, []);
        return obj;
      });
  }
}

These adjustments enable the data service to handle all types of objects and arrays.

Example If the API response contained the following JSON:

{
    "uniqueId": 1,
    "name": "Daniel",
    "courses": [
        {
            "uniqueId": 123,
            "name": "CS 101",
            "summary": "An introduction to Computer Science",
            "beginDate": "2018-04-20"
        }
    ]
}

The console would display the following messages: errors

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

Discovering the keycode for the GO button in JavascriptDiscovering the keycode

Can anyone help me figure out how to find the keycode for the GO button using Javascript in an Android browser? ...

Color picker can be utilized as an HTML input element by following these steps

After trying out various color pickers, I was not satisfied with their performance until I stumbled upon Spectrum - The No Hassle jQuery Colorpicker. It perfectly met my requirements. <html> <head> <meta http-equiv="content-type" content="t ...

Confirmation of numerous checkbox selections

I am facing a challenge with a form that contains multiple questions answered through checkboxes. I need to validate that at least one checkbox is selected in all the questions, otherwise an alert message should pop up. Is it possible to achieve this val ...

What is the process for setting data to the value attribute in an input tag, retrieved from a firebase database?

I utilized the following code snippet to retrieve data from Firebase database and store it in the Name variable. var userId = localStorage.getItem('keyName'); var dbRefName = firebase.database().ref() .child(& ...

Using NextJS's API routes to implement Spotify's authorization flow results in a CORS error

I am currently in the process of setting up the login flow within NextJS by referring to the guidelines provided in the Spotify SDK API Tutorial. This involves utilizing NextJS's api routes. To handle this, I've created two handlers: api/login.t ...

Retrieve the present cursor location in a multiline textbox without the need to select any text

Is there a way to determine the current cursor position in a multi-line textbox without selecting any data within the textbox? ...

JavaScript embedded in an HTML document, which in turn is embedded within JavaScript

Is it possible to nest tags within other tags to control the functionality of a download button in a chat bot? Unfortunately, nesting tags is not allowed, so I'm looking for an alternative solution. Below is the complete HTML file I'm working wit ...

During the installation of Angular CLI, an error occurred indicating an unexpected ending of JSON input while parsing near the string '...gsJjnNLbV xrOnxOWiCk'

C:\Users\BB>node --version v14.15.0 C:\Users\BB>npm --version 8.3.2 npm install -g @react/react-native npm ERR! Error: Unable to find module '@react/react-native' npm ERR! A detailed log of this process can be locate ...

A guide to showcasing items based on their categories using React.js

After successfully displaying Categories from a port (http://localhost:5000), accessing my MongoDB database, I encountered an issue when attempting to display the products for each category separately. Despite trying the same method as before, I keep rec ...

What causes the occurrence of "undefined" after multiple iterations of my code?

I've encountered a curious issue with the code snippet below. Everything seems to be running smoothly except for one thing - after a few iterations, I start getting "undefined" as an output. You can test this for yourself by running the code multiple ...

Ways to position the navigation menu alongside a search bar and a dropdown tab?

My navigation bar includes a search form, multiple links, and a dropdown link leading to additional links. Unfortunately, I'm struggling to align everything on the same line within the navbar. This is the current output: View My HTML Page Below is ...

Having trouble with a Vue3 project after running a Vite build in production mode?

When I run my project in development mode, everything works perfectly fine. However, when I build the project using `vite build´, some components stop functioning. Oddly enough, there are no errors displayed in the console or in the build logs. If I use ...

Retrieving live information from an API in order to populate a specific route

Utilizing the contents of two crucial files to extract data from separate APIs, here is my simplified example: poloniex.js const Poloniex = require('poloniex-api-node') const poloniex = new Poloniex() async function obtainExchangeData() { po ...

Exploring creative solutions for generating PDFs with Node JS

Looking for a way to generate PDF Documents in Node.JS? Is there an alternative solution for organizing templates for various types of PDF creation? I've been utilizing PDFKit for creating PDF Documents on the server side with Javascript. Unfortunate ...

How can I extract specific data from a JSON response and populate a select dropdown with AngularJS?

My current project involves making an http.get request to fetch a list of airports. The JSON response is packed with information, presenting a series of objects in the format shown below {"id":"1","name":"Goroka","city":"Goroka","country":"Papua New Guine ...

A possible invocation of a function using a JavaScript "class"

Presented here is a node module I've been working on: var _ = require('lodash'); function Config(configType, configDir) { this.configType = configType; this.configDir = configDir; }; Config.prototype.read = function() { // read file t ...

The error message "THREE is not defined" occurred while running mocha tests for a

I am attempting to execute a basic Mocha unit test for code that utilizes the Vector3 class from THREE.js: import {Vector3} from 'three'; const a = new Vector3(0, 0, 0); When running this test using Mocha (specifically mocha-webpack, with webpa ...

How to set up an Angular ErrorHandler?

Attempted to register an Angular ErrorHandler in this simplified StackBlitz environment, but it seems that it's not capturing the error thrown within the constructor of the HelloComponent. Any insights on this issue? Your opinions? ...

Having trouble retrieving object properties within an Angular HTML template

There are two objects in my code that manage errors for a form: formErrors = { 'firstname': '', 'lastname': '', 'telnum': '', 'email': '' } ValidationMessa ...

Strange behavior observed in Angular Reactive form

I've been troubleshooting this code snippet: login.component.html <div class="main"> <div class="image"> <img src="./assets/icons/login.png" alt="Login"> <p>Sign in t ...