Solvedangular 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
  {
    return this.http.get('/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();
      })
  );
});
```
46 Answers

✔️Accepted Answer

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).

Other Answers:

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.

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.

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.

More Issues: