angular: HttpClient mapping to typescript types not working

widget.service.zip

widget.service.zip

I’m submitting a…


[ ] Regression (a behavior that used to work and stopped working in a new release)
[X] Bug report  
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

With HttpClient automatically converting http responses to typescript types, it does not do any type casting to ensure that they are valid according to the defined types. I presume this is happening because the final result is javascript, which doesn’t enforce any type checking.

Expected behavior

I would expect that a json “1” which is defined as a typescript “number” would be converted to said number, but instead it remains as a string “1”.

Minimal reproduction of the problem with instructions

  • ng new invalidtypes
  • cd invalidtypes
  • copy widgets.service.ts and widgets.service.spec.ts into the app folder
  • ng test

What is the motivation / use case for changing the behavior?

The purpose of using typescript, is to have some sort of strict type checking, so as to avoid human errors. It makes sense to ensure types are accurate internally as well, or there is confusion when things like this occur.

Environment


Angular version: 5.0.0 and 5.0.5


Browser:
- [X] Chrome (desktop) version 62.0.3202.89
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
 
For Tooling issues:
- Node version: XX  
- Platform:  
$ node --version
v8.9.0

$ uname -a
Linux trenta-VirtualBox 4.8.0-53-generic #56~16.04.1-Ubuntu SMP Tue May 16 01:18:56 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

$ cat /etc/issue
Linux Mint 18.2 Sonya \n \l

Below is the widget service with a failing test comparing the id to 1, which should return true, but because the 1 is a string, it fails.

```
// widget.service.ts
import { Injectable } from '@angular/core';
import {Observable} from "rxjs/Observable";
import {HttpClient} from "@angular/common/http";

class Widget {
  id: number;
  title: string;
  widgetInfo: string;
  activityDate: Date;
  createDate: Date;
  user: number;
}

@Injectable()
export class WidgetsService {

  constructor(private http: HttpClient) { }

  getWidgets(): Observable<Widget[]>
  {
    return this.http.get<Widget[]>('/widgets');
  }
}
```

```
// widget.service.spec.ts
import {TestBed, inject, getTestBed} from '@angular/core/testing';

import {WidgetsService} from './widgets.service';
import {
  HttpClientTestingModule,
  HttpTestingController
} from "@angular/common/http/testing";
import {HttpClient} from "@angular/common/http";

describe('WidgetsService', () => {
  let widgetsService: WidgetsService;
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [WidgetsService]
    });
    let injector = getTestBed();
    widgetsService = injector.get(WidgetsService);

  });

  it('should be created', inject([WidgetsService],
    (service: WidgetsService) => {
      expect(service).toBeTruthy();
    }));

  it('should get widgets with proper types',
    inject([HttpClient, HttpTestingController],
      (http: HttpClient, httpMock: HttpTestingController) => {
        widgetsService.getWidgets().subscribe(widgets => {
          expect(widgets[0].id).toEqual(1);
        });

        const req = httpMock.expectOne('/widgets');
        expect(req.request.method).toEqual('GET');
        req.flush(
          [{
            "id": "1",
            "title": "a widget",
            "note": "the long text about the widget",
            "activityDate": "2017-11-27T13:44:50.750Z",
            "createDate": "2017-11-27T13:44:50.750Z",
            "user": null
          }, {
            "id": "2",
            "title": "another widget",
            "note": "the long text about the widget",
            "activityDate": "2017-11-27T13:44:50.751Z",
            "createDate": "2017-11-27T13:44:50.751Z",
            "user": null
          }]
        );
        httpMock.verify();
      })
  );
});
```



About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 6
  • Comments: 45 (11 by maintainers)

Most upvoted comments

I agree with all the previous comments, I find the syntax misleading. I expect that

this.httpClient.get<Model>(url)

verifies if the data coming from the get can be mapped to Model and if not then throws. The current implementation looks and feels javascript, not typescript (imho).

TypeScript only verifies the object interface at compile time. Any object that the code fetches at runtime cannot be verified by TypeScript.

If this is the case, then things like HttpClient.Get<T> should not return Observable<T>. It should return Observable<Object> because that’s what is actually returned. Trying to state that it returns T when it returns Object is misleading.

the client’s return says this: @return an `Observable` of the body as type `T`.

In reality, it should say: @return an `Observable` of the body which might be `T`. You do not get T back. If you got T back, it would actually be T, but it’s not.

To look at it another way if I wrote this:

export class Widget<T> {
    getValue(input: string): T {
         return new T(string);
    }
}

let thisShouldBeADate = new Widget<Date>().getValue("2017-01-01T02:00:00");

I would expect the call to the method to return a Date object. If it for some reason returned a string, I’d be exceptionally confused.

At the very least the JSDocs for HttpClient need to change to note that the generic is there purely for the IDE and has no meaningful impact on the actual object returned and that it’s up to the developer to create translation objects/methods/interceptors for all their HttpClient calls the contain anything other than base types.

I’m a bit confused.

Angular accepts a type like this… this.http.get<Widget[]>(‘/widgets’);

Why does it do that, if it’s not the intention of angular to provide mapping from json to typescript? It certainly lead me into believing that it fully supported mapping json to typescript objects, but clearly doesn’t. That’s fine if it doesn’t, but it wasn’t clear.

The Widget is indeed modeled to fit the type of the object that I want, it’s just that the final result was a string, not a number.

Still no HttpClient auto-converting JSON to the actual TypeScript class. There is absolutely NO excuse that Angular hasn’t provided this obvious professional feature yet. This has been implemented for YEARS in Java frameworks and as a reference see Java JAXB API and Jackson API and how it should be done. HttpClient should do ALL the magic and return the actual correct no matter how it is technically implemented or whatever excuse any developer can come up with. Make it happen Angular team.

If a piece of software uses TypeScript, that piece of software should also deal with the subtleties of TypeScript, or simply not use it. Not doing so causes confusion. One is professional, the other is not.

It’s not TypeScript’s job to recognise/convert data to the correct type. It’s your job to ensure that the data returned from the API matches what you want.

TypeScript only verifies the object interface at compile time. Any object that the code fetches at runtime cannot be verified by TypeScript. Angular packages are merely a bunch of .js files and corresponding .d.ts declarations files. Magic (that HttpClient verifies your model) does not happen (without additional effort). IMHO anyone with further questions should learn what TypeScript is.

Oh, most definitely, I found that useful too. It just ended up leading me to believe that the object I was using was type safe (the purpose of typescript), when it wasn’t. The purpose of generics is to create type safety, IDE auto-completion is just a convenient side affect of generics, not it’s intended purpose.

So, is there a way we could take it a step further, and incorporate some sort of mechanism to either ensure type safety, or provide JSON.parse() reviver functions to the process?

While I agree it’d be nice for it to handle it. I’d just be happy with the help docs not implying that it is handling it, when it isn’t.

You can use the exact same paragraph to argue the other way. By asserting that HttpClient is saying “Trust me I know what i’m doing”. It all comes down to where the responsibility lies to ensure that the object you get back is what you expect it to be. The way the documentation, examples, etc are laid out, it is inferred that you get back an object of type T when you use HttpClient with generic parameters. I know that isn’t the case, but the JSDocs state exactly the opposite of what you are saying.

So 1 of 2 things should change.

  1. The JSDocs should not say that it returns T. This is dead simple to do, requires no coding changes, and doesn’t change in any way how Angular works. It purely helps new developers realize that generics don’t work the same way in Typescript as in other languages and that Angular is passing the responsibility to the developer to take the thing that is similar to T and make it an actual T. This is OK. But if the docs says “Returns T”, then it had better well return T.

  2. The methods should actually return T. This is harder, though I imagine the vast majority of the time, the most complex thing that can’t be easily returned is the Date object.

@iamwyza Interestingly your last paragraph reveals the anti-pattern of it all; which is something to compensate for a bad design pattern. It’s like saying “Because the angular developers decided they didn’t want to do it the right way, the least they can do is let us know so we can hack around it”. I’m not saying that to be insulting, but that is indeed what’s happening.

“An anti-pattern is a common response to a recurring problem that is usually ineffective and risks being highly counterproductive.” https://en.wikipedia.org/wiki/Anti-pattern

@TrentonAdams yes - generate your interfaces from some backend schema and ensure your backend is following that same schema. A lot of projects within Google use this approach with HttpClient.

When you pass types into HttpClient, it provides you with type information when you use the result onwards. It prevents you from typing result.wigtInfo, for example.

I don’t think it is the responsibility of Angular to check the validity of data types. The HttpClient just parses the response to a json object and handles it to your code. Your Widget interface should be modeled to fit the type of the object.

Im with @TrentonAdams . The syntax certainly gives the impression that the data returned would be of the type specified. (this.httpClient.get<User>(‘/user’)). From the comments above, i get the impression that this new syntax doesn’t actually do anything. Is that correct?

I hate to even jump in on this discussion as it seems to have devolved into personal attacks but I am hoping that people on both sides can help me answer what I think is more real-world example of where some type checking would be extremely valuable. Please consider the following example:

The following JSON is returned by a data service somewhere:

{
    id: 1234,
    name: "Testy Testerson",
    createdUTC: "2018-07-12T16:00:00Z"
}

The following model exists in our project:

export interface SimplePerson {
    id: number;
    name: string;
    createdUTC: Date;
}

The following code exists in our project:

this.http.get<SimplePerson>(apiBase + "/person/1234").subscribe(person => {
    var x = person.createdUTC.getSeconds(); // This line will compile but will error at runtime
    var y = person.createdUTC.slice(4); // This line won't compile but would work at run time
});
  • There isn’t a problem at the server because it returned well formatted JSON that properly maps the expected object.
  • There isn’t a problem with the model, it perfectly matches what the data service returned.

My Conclusion

Dates are where this issues comes alive in biggest way for me. It would be amazing if Angular would take the returned data, and since we’ve informed the HttpClient of the type we’re expecting back, attempt to map it correctly. I’m not an expert, just a daily user and this is a very common problem.

Suggestions welcome!

Coming from Java Spring it’s very misleading. Even with the examples you would expect it to return the correct type and not just the values. Is this a limitation of TS or just Angular?