'Explicitly typed array doesn't seem to be being populated from the Http.Get as that type. Angular, Typescript, Javascript

So I have two classes defined for this project:

CustomerJob

export class CustomerJob {
    partNumber?: string;
    revision?: string;
    description?: string;
    dueDate?: Date;
    customer?: string;
    purchaseOrderNumber?: string;
    quantityDue?: number;
    quantityShipped?: number;
    salesOrderNumber?: number;
    salesOrderLineNumber?: number;
    salesOrderLineLotNumber?: number;
    itemID?: number;
    salesOrderCreateDate?: Date;
    salesOrderLineCreateDate?: Date;
    salesOrderLineLotCreateDate?: Date;
    progress?: number;
    shipVia?: string;
    status?: string;
    customDescription?: string;
    shippingLocation?: string;

    documents?: DocumentFile[];
    openQuantity?: number = 0;

}

DocumentFile

export class DocumentFile {
    id?: number;
    url?: string;
    fileName?: string;
    extension?: string;
    extensionWithoutPeriod?: string;
    isImageFile: boolean = false;


    setup () {
        console.warn('doing setup for document');
        // get name of file (filename.extension). need to remove all the \\ stuff basically
        let _stringArray = this.url?.split('\\');
        // the last one in this split is the filename and extension
        let _filenameWithExtension = _stringArray![_stringArray!.length - 1];

        // split this by period
        let _filenameSplit = _filenameWithExtension.split('.');

        // first one in array is the filename
        this.fileName = _filenameSplit![0];
        // second is extension
        this.extensionWithoutPeriod = _filenameSplit![1];
        this.extension = '.' + this.extensionWithoutPeriod;
    }
}

I have a service that looks like this:

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { CustomerJob, DocumentFile } from '../dataModels/data-models';
import { LoginService } from '../services/login.service';

@Injectable({
  providedIn: 'root'
})
export class JobService {

  constructor(private http: HttpClient, private loginService: LoginService) { }


  jobs?: CustomerJob[] = [];


  async getCustomerJobs () {
    let headers = new HttpHeaders().set('APIKey', this.loginService.loginInformation!.apiKey);

    try {
      this.jobs = await firstValueFrom(this.http.get<CustomerJob[]>('https://XXXXX', { headers: headers }));

      // do some modifications to them
      this.jobs.forEach(function (j) {
        // quantity
        if (j.quantityDue != undefined && j.quantityShipped != undefined) {
          j.openQuantity = j.quantityDue - j.quantityShipped;
        }

        // document stuff
        if (j.documents != undefined) {
          j.documents.forEach(d => {
            // console.warn('setting up F');
            if (d == undefined) {
              console.warn('this document is undefined');
            }
            else {
              //d = Object.assign(d, j.documents[i]);

              console.warn('this document is defined: ' + d.id + ', ' + d.url);

              d.setup();

              console.warn(d.fileName);
            }   
          });


         
        }
        else {
          console.warn('this one DOES NOT have documents');
        }

      });

    }
    catch (error: any) {
      console.error('CATCH ERROR: ' + error.message);
    }

    return this.jobs;

  }
  

}

This is the error I get in the console via the TryCatch:

d.setup is not a function

The important thing here though is that it IS properly printing out some info in the line right before it:

console.warn('this document is defined: ' + d.id + ', ' + d.url);

So eventually I figured out that the problem must just be that "d" isn't actually a DocumentFile type. I can't figure out why since the array is typed specifically as a DocumentFile array in the CustomerJob class. So at some point, I'm guessing when it's creating the array of CustomerJobs through the Http.Get it's just transforming the specifically typed DocumentFile array into an array of anonymous objects or whatever.

I know this is true because if I change the code to look like this it works fine:

j.documents.forEach(d => {
            // console.warn('setting up F');
            if (d == undefined) {
              console.warn('this document is undefined');
            }
            else {
              let doc: DocumentFile = new DocumentFile();
              Object.assign(doc, d);

              console.warn('this document is defined: ' + doc.id + ', ' + doc.url);

              doc.setup();

              console.warn(doc.fileName);
            }   
          });

I come from using C#, so I always have trouble with loosely typed languages like JavaScript. It's very frustrating. Thank you in advance for your help.



Solution 1:[1]

Your problem is that when the HttpClient deserializes the results from the server, it has no way of knowing what form you actually want it to be in. TypeScript interfaces and types are purely compile time artifacts, unlike in C# where you can use reflection. If you open up the dev tools and look at the results from your API call, you'll see your data is pure JSON. That is what HttpClient sees as well.

TypeScript interfaces at the networking boundary are just documentation, it is up to you to write the documentation to match what is actually coming from the server.

I would recommend changing your DocumentFile class to be an interface, moving the setup code to a regular function definition, and just calling it like:

setup(doc);

It might seem weird coming from C# that you just have a function definition hanging around, but I assure you it's perfectly normal and a pretty standard way of handling this kind of thing.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1