Search code examples
angulartypescripthttpclientbuild-error

can't build angular app due to httpClient response data array throwing typescript build issue


Hello been out of the coding game for a few years and I am ramping up on Angular 8 (not enjoying it!) I can run ng serve and everything is ok but if I try to build I am getting an error here

userD() {
  // TODO User Angular Service will get to that later.
  var aToken = localStorage.getItem('token');
  const headers = new HttpHeaders().set("Authorization", "Bearer " + aToken);

  this.httpClient.get( 'http://127.0.0.1:8000/api/auth/user', { headers } )
  .subscribe((res: any[]) => {
    // @ts-ignore
    this.users = res.data.result; // it doesn't like the .data.result part in build
    console.log(this.users);
  });
}

I am using //@ts-ignore for now, not ideal but I don't know why it has a problem with going in to the array if i have it as res it's ok.

Any information would be very helpful to help me understand this. Thank you.

NB: EDIT Here is the json and I want to extract to this results object to then use For in the html to loop through {"status":"200", "data": {"result":{"id":3,"name":"patrick lord","email":"patrick@larvel.io","email_verified_at":null,"phone":"123456789","profilePic":71,"created_at":"2019-09-19 19:43:04","updated_at":"2019-09-19 19:43:04","stripe_id":null,"card_brand":null,"card_last_four":null,"trial_ends_at":null,"gender":"male","Address1":"i don't live here S","Address2":"232","City":"Clearfield","Country":"USA","Zip_Post":"55550","YouTube"}}}


Solution

  • The Explanation

    JB's comment hits the nail on the head: you're getting a TypeScript error because the any[] type doesn't have a data property; by definition, any[] is expecting anything, with the only restriction being that the input must be an object of type Array.

    But why the error in build --prod and not serve?

    You're getting the error on ng build --prod and not on ng serve is because the default Angular environment enforces strict (as in "strict adherence to typing and syntax") on --prod, but doesn't enforce strict in the local dev environment. This is so you can play around and figure stuff out more easily when developing locally, where sloppy syntax is less of an issue, before cleaning up your code for production use. It took me a bit to wrap my head around this, too, but it's a Good Thing(tm) :P

    The Justification

    JavaScript lets you do pretty much whatever you want: it has no clue what res is going to look like, but it assumes you know what you're doing and if you say res is going to have a property called data, it just rolls with it.

    This is great if you've got a small project, or if you're the only person who's ever going to work on it, but it starts to get tricky when the project gets larger, or if you have multiple developers submitting code. You know that res has a data property, but how does Samantha know that?

    Enter TypeScript

    TypeScript is JavaScript...with Types. Types provide the shape of a given object, so everybody involved (including the TypeScript transpiler) knows what's up. When you say res: any[] , what TS hears is "object res is going to be an Object of type Array"; we haven't, however, told TS anything about the member-items within the array, and since it doesn't know what kind of array res is going to be, the only things to which we have access are the generic methods and properties of the Array type, itself.

    The Fix

    So, what do we need to do instead of (res: any[]) to make this.users = res.data.result; work?

    Define an Interface for the ResponseObject

    We need to start by telling TS what to expect with a little more detail:

    export interface ResponseObject {
      data: {
        result: {};
      }
    }
    

    Now TS knows that there's an Object called ResponseObject that has (at minimum) a data property, and that data property is itself an Object that has (again, at minimum) a property called result.

    Tell TypeScript to expect our ResponseObject

    We can plug this into our subscription call like this:

    .subscribe( (res: ResponseObject) => {
      this.users = res.data.result; // now it totally DOES like the .data.result part in build --prod!
      console.log(this.users);
    });
    

    Wait a sec...

    You may notice that res is no longer expected to be an object of type Array; based on the code you posted and the fact you said it worked via ng serve, it sounds like you're expecting a single ResponseObject that delivers an array of UserObjects as the payload on the data.result property; if so, the code sample above should work fine.

    If, however, our .get() really is returning an array of ResponseObjects, we're going to need to do some extra work.

    Dealing with an array of ResponseObjects

    Let's change up our interface declaration, first:

    export interface ResponseObject {
      status: number;  /*  It's OK to exclude things we don't need,
          but, the status property is going to come in handy when
          we eventually want to write a catch statement for cases
          when the call returns an error instead of the data we
          wanted  */
      data: {
        /*  you can also split up your interface declarations to
            make them easier to read; this has the added benefit of
            allowing us to more easily access the sub-properties of
            our payload later on  */
        result: UserObject;  /*  we're expecting a single
            UserObject per ResponseObject in this scenario,
            otherwise we'd use:  */
        // result: UserObject[];
      }
    }
    
    export interface UserObject {
      /*  remember to use the generic type names (lowercase 'string'),
          not Primitives (Capitalized 'String') in your interface
          declaration, otherwise you'll get more errors  */
      id: number;
      name: string;
      email?: string;  /*  the question mark lets you define a
        property that *may* exist, but may not.  Useful if your
        target API only includes parameters with non-null values
        in the ResponseObject   */
      // ...all the other things you want and think will exist
    }
    

    Then we can mix up our subscription call:

    .subscribe(
      // we tell subscribe that we're expecting an array of ResponseObjects
      (res: ResponseObject[]) => {
        /*  users needs to be defined as an array to hold onto
            multiple UserObjects; we're clearing it here to make
            sure we're only dealing with fresh data and specifying
            that it will be an array of type UserObject  */
        this.users: UserObject = [];
    
        /*  the data property isn't a property of the *array*, but
            of each array *member-item*, so we need a loop  */
        res.forEach(
          (res_item: ResponseObject) => {
            // add the response object data to our users array
            this.users.push(res_item.data.result);
        });
    
        // now console log should properly show our array of UserObjects!
        console.log(this.users);
      }
    );
    

    n.b., Please don't forget to mark this as the Accepted Answer if it does, in fact, answer your question satisfactorily. If someone comes along with a better or more concise answer to your question you can always change the Accepted Answer after-the-fact.

    Cheers!