3

I'm scrutinizing the docs for HttpClient, focusing on the get(...) method. I've prepared the following sample:

const headers: HttpHeaders = new HttpHeaders();
const observe: HttpObserve = null;
const params: HttpParams = new HttpParams();
const reportProgress = false;
const responseType = "json";
const withCredentials = true;

const options = {
  headers, observe, params,
  reportProgress, responseType, withCredentials
};

this.http.get(url, options)

I get an error stating the following.

No overload matches this call.  
The last overload gave the following error.  
Argument of type '{ responseType: string; ... }'  
is not assignable to parameter of type '{ responseType?: "json" | undefined; ... }'.  
Types of property 'responseType' are incompatible.
Type 'string' is not assignable to type '"json" | undefined'.

It's pretty obvious what's the reported issue. However, I don't see how what I typed is in validation towards what is required. If I type undefined as the value for responseType, the compiler is satisfied. In fact, the elaborated code samples (number 7, 8 and 12 through 15) explicitly state that it's the syntax to be used.

How is my "json" not the required "json"?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Konrad Viltersten
  • 36,151
  • 76
  • 250
  • 438
  • Because responseType is typed as *string*, that's what the error message tells you. It's not typed as the string literal type `"json"`. – jonrsharpe Jun 30 '20 at 06:48
  • It should have been inferred as the literal type since [TS 2.1](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#better-inference-for-literal-types), though - what version are you using? – jonrsharpe Jun 30 '20 at 07:00
  • @jonrsharpe My VSCode says v3.9.4 at the bottom. When I ran *tsc --version*, I got an error but it's likely due to being in the wrong directory. I trust VSC on this one. Also, I'm not entirely sure what you mean by *literal type* so I'll google it. I thought that a *string literal type* and a constant *string* were different names for the same concept. – Konrad Viltersten Jun 30 '20 at 07:08
  • https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types - `"json"` is a `string` but not all `string` are `"json"`. – jonrsharpe Jun 30 '20 at 07:09
  • @KonradViltersten: By literal type he means type `json` instead of `string`. One workaround could be `const responseType = "json" as "json";` – ruth Jun 30 '20 at 07:10
  • It works in 3.9.2 on the playground: https://www.typescriptlang.org/play/index.html#code/GYVwdgxgLglg9mABFAFjMBzAFHADlAZwC5EBvRPWBAfhIHIArAhOxAXwEoy2AoHiBASgV88JAF5EjZmDoBuPqnTZylMew4KgA. Could be a bug in 3.9.4, maybe see if anyone's reported it. – jonrsharpe Jun 30 '20 at 07:10
  • 1
    @MichaelD prefer `const responseType: "json" = "json";` - then the *actual value* gets checked (compare `const foo = "bar" as "baz"`). – jonrsharpe Jun 30 '20 at 07:11
  • @MichaelD I feel a bit retarded now. Are you saying that my string should contain a valid JSON syntax (i.e. brackets, properties, colons and value, e.g. `{a:1}`)? Also, `as "json"` felt unfamiliar to me. As far my competence reaches, there's no `Json` type/class in TS. Or is it som black magic (black magic to ignorant me, that is) saying that the type is *whatever*, where said *whatever* happens to be *"json"* sequence of characters? – Konrad Viltersten Jun 30 '20 at 07:18
  • @KonradViltersten no they're not saying that at all, just missing quotes from `"json"`. If you don't know what `as ` means read up on [type assertions](https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions). You really need to know TS basics to be effective in Angular. – jonrsharpe Jun 30 '20 at 07:20
  • Oh I get it, it's because `{ responseType }` widens to `{ responseType: string }` - https://www.typescriptlang.org/play/index.html#code/GYVwdgxgLglg9mABFAFjMBzAFHADlAZwC5EBvRPWBAfhIHIArAhOxAXwEoy2AoHiBASgV88JAF5EjZmDoBuPgLBCRVZYknlKY9gp6p02bYI4KgA. – jonrsharpe Jun 30 '20 at 07:23
  • @KonradViltersten: Not exactly. Think of string literal type (`json` here) with union types (`"json" | "text" | ...`) akin to enums. So the `responseType` should have the exact string `"json"`. But for some reason (which I too don't understand yet), it's being inferred as type `string` in your case. So one solution would be to assert the type using the [`as `](https://stackoverflow.com/a/42551933/6513921) syntax or better as @jonrsharpe suggested define the type before assigning the value: `const responseType: "json" = "json";`. – ruth Jun 30 '20 at 07:24
  • @KonradViltersten it's literally on the "basic types" page... – jonrsharpe Jun 30 '20 at 07:25
  • @MichaelD I must admit, I feel a weird satisfaction and relaxation of the mind when other developers whom I trust say things like "*I **too** don't understand*" as it provides me the notion of not being a single dumb dude on the block but actually hitting something that's at least a little bit confusing. I hate being the **only** confusee. :) – Konrad Viltersten Jun 30 '20 at 07:37

1 Answers1

2

The HttpClient methods use string literal types for some of the options - rather than just declaring e.g. responseType as the generic string they provide the specific values it can take. So why does your options object not meet the type definition, given it has one of the accepted values?

The initial declaration:

const responseType = "json";

defines responseType as the string literal type "json"; it's a const, it can only ever have that single value. So far, so good. However, the object declaration:

const options = { responseType /* etc. */ };

gives options the type { responseType: string }, it widens the type of the attribute. It does this because objects are mutable, so you could change the value.

To fix this, you have several options; in no particular order:

  • Inline the object creation:

     this.http.get(url, { responseType });
    

    This doesn't widen the type, because you can't assign a different value into an object you don't hold a reference to.

  • Explicitly type the intermediate object:

     const options: { responseType: "json" } = { ... };
    
  • Use a const assertion on the object:

     const options = { responseType } as const;  // or = <const>{...};
    

    This tells the compiler you aren't going to change the values and gives options the type { readonly responseType: "json" }.

  • Use a const assertion on the string (suggested by Michael D):

    const responseType = "json" as const;  // or = <const>"json";
    

    This one's a bit weird, because the type of responseType is still "json", as it was originally. However this creates a "const context" in which that type is non-widening, so the resulting type of options is { responseType: "json" } (not readonly as above, but you can only assign that one value to it).

Here is a playground showing the various options for options: TypeScript Playground.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • This is rather interesting. I'd also add the type cast solution: `'json'`. – ruth Jun 30 '20 at 07:36
  • @MichaelD that does seem to work, `const responseType = "json"` (or `"json" as const` to use the syntax in the answer), but I don't understand *why* so I don't really want to add it - the type of `responseType` doesn't change, it's still `"json"`, but that `` seems to impact the object creation too, that metadata somehow stays with the value. It makes the `options` type `{ responseType: "json" }`, which isn't `readonly` like `as const` on the object but can still only take that one value. – jonrsharpe Jun 30 '20 at 07:40
  • @MichaelD alright I figured it out and added it, thanks! – jonrsharpe Jun 30 '20 at 08:24