angular: HttpParams of New HttpClient Doesn't Support Number, Boolean and Date

I’m submitting a…

[x] Regression (a behavior that used to work and stopped working in a new release)

Current behavior

ERROR in /path/to/heroes.service.ts(23,44): error TS2345: Argument of type '{ params: { groupId: number; }; }' is not assignable to parameter of type '{ headers?: HttpHeaders | { [header: string]: string | string[]; }; observe?: "body"; params?: Ht...'.
  Types of property 'params' are incompatible.
    Type '{ groupId: number; }' is not assignable to type 'HttpParams | { [param: string]: string | string[]; }'.
      Type '{ groupId: number; }' is not assignable to type '{ [param: string]: string | string[]; }'.
        Property 'groupId' is incompatible with index signature.
          Type 'number' is not assignable to type 'string | string[]'.

Expected behavior

No errors should occur.

Minimal reproduction of the problem with instructions

// heroes-query-params.model.ts
export class HeroesQueryParams {
    groupId: number;
    country: string;
    dead: boolean;
}

// heroes.component.ts
const params: HeroesQueryParams = {
    groupId: 1,
    name: 'US',
    dead: false
};
this.heroesService.getHeroes(params).subscribe((heroes: Heroes) => {
    console.log(heroes);
});

// heroes.service.ts
import { HttpClient } from '@angular/common/http';
// ...
getHeroes(params: HeroesQueryParams): Observable<Heroes> {
    return this.httpClient.get<Heroes>('/path/to/heroes', {
        params: params // ERROR
    });
}

To cut a long story short, new HttpClient, which is imported from @angular/common/http, doesn’t support non-string values in query params. This is related to numbers, booleans, dates etc. That’s why I should use old one, write some transform utils or just .toString().

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

It worked previously.

Environment

Angular version: 6.0.0

Browser:
- [x] Chrome (desktop) version 66

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 23
  • Comments: 21 (4 by maintainers)

Commits related to this issue

Most upvoted comments

This is for those who want fast solution.

// utils.service.ts
import { HttpParams } from '@angular/common/http';
// ...
export class UtilsService {
    static buildQueryParams(source: Object): HttpParams {
        let target: HttpParams = new HttpParams();
        Object.keys(source).forEach((key: string) => {
            const value: string | number | boolean | Date = source[key];
            if ((typeof value !== 'undefined') && (value !== null)) {
                target = target.append(key, value.toString());
            }
        });
        return target;
    }
}

// heroes.service.ts
import { HttpClient } from '@angular/common/http';
// ...
getHeroes(params: HeroesQueryParams): Observable<Heroes> {
    const queryParams: HttpParams = UtilsService.buildQueryParams(filter);
    return this.httpClient.get<Heroes>('/path/to/heroes', {
        params: queryParams
    });
}

FIX THIS!

@nhays89, instead of yelling you’d better create pull request with fix.

@nhays89, instead of yelling you’d better create pull request with fix.

HttpParams is immutable. set() creates and returns a new HttpParams instance, without mutating the instance on which set() is called. So the code should be

const params = new HttpParams().set(‘status’, status);

This is not a regression, since HttpClient is explicitly not API-compatible with the old @angular/http library. However, it is something we should support.

@gautamkrishnar, I think there won’t be any. It seems like Angular team sees us using new HttpParams().append(key, nonStringValue.toString()) each time, so I just proceed with my solution above.

HttpParams is immutable. set() creates and returns a new HttpParams instance, without mutating the instance on which set() is called. So the code should be

const params = new HttpParams().set(‘status’, status);

@gautamkrishnar, I think there won’t be any. It seems like Angular team sees us using new HttpParams().append(key, nonStringValue.toString()) each time, so I just proceed with my solution above.

this OOP enthusiasm is definitely went too far in this case

Still no update on this?

Finally, here’s the solution.

// utils.service.ts
import isNil from 'lodash/isNil';
import isPlainObject from 'lodash/isPlainObject';

import { HttpParams } from '@angular/common/http';
// ...
export class UtilsService {
    static buildQueryParams(source: Object): HttpParams {
        let target: HttpParams = new HttpParams();
        Object.keys(source).forEach((key: string) => {
            let value: any = source[key];
            if (isNil(value)) {
                return;
            }
            if (isPlainObject(value)) {
                value = JSON.stringify(value);
            } else {
                value = value.toString();
            }
            target = target.append(key, value);
        });
        return target;
    }
}

// heroes.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';

import { UtilsService } from '/path/to/utils.service';

import { Heroes } from '/path/to/heroes.model';

@Injectable()
export class HeroesService {
    constructor(private httpClient: HttpClient) {
    }

    getHeroes(params: any): Observable<Heroes> {
        const queryParams: HttpParams = UtilsService.buildQueryParams(params);
        return this.httpClient.get<Heroes>('/path/to/api/heroes', { params: queryParams });
    }
}

Here I used two helper methods from Lodash.

@AlainD-, thanks for pointing this out. It really depends on the client-server contract. For example, maybe you want Date.now().toString() instead of new Date().toISOString(). In this solution I wanted to mimic AngularJS behavior, so I need to take a look at its date auto convertion and replicate it. What’s even more important, is to safely handle reference types: Object, Array and Function. Arrays work as expected, because [1, 2, 3].toString() returns '1,2,3', but it’s not the case for objects and functions. Probably objects auto conversion should be just JSON.stringify({ foo: 'bar' }). And for functions I think we may leave toString(). What’s your opinion? Am I overcomplicating this?

@ericmartinezr seems like #19595 wont be updated