skip to content

In Angular, what does httpResource's parse option do that httpResource<Review[]>() alone does not, and how does it work with httpResource.text() or blob()?

level: middleimportance: should knowfreq 36%

answer

  1. a generic is only a promise
  2. the raw value arrives as unknown
  3. the return type becomes the value type
  4. a throw becomes the error state

basics

~20 s

httpResource<Review[]>() only asserts a type. parse receives the raw body (unknown for JSON, string for text(), Blob for blob()), validates or transforms it, and its return type becomes the value type; a throw puts the resource in error.

solid answer

~40 s

`httpResource<Review[]>(...)` is a compile-time **assertion**: the JSON body is handed over as `Review[]` without any check. The `parse` option adds a runtime step. Angular calls it with the **raw** value — `unknown` for the default JSON form, `string` for `httpResource.text()`, `Blob` for `httpResource.blob()`, `ArrayBuffer` for `httpResource.arrayBuffer()` — and whatever it returns becomes `value()`, with its **return type inferred as the resource's type**. That makes it the place to validate with a type guard or a schema library's parse function, and to transform: convert ISO strings to `Date`, rename fields, split a CSV. If `parse` throws, the resource enters the `error` status with that error in `error()`, even though the HTTP call succeeded. `parse` also runs on a response restored from the SSR transfer cache.

code

ts · 46 lines
ts
import { Component, input } from '@angular/core';
import { httpResource } from '@angular/common/http';

export interface Review {
  id: string;
  rating: number;
  postedAt: Date;
}

function parseReviews(raw: unknown): Review[] {
  if (!Array.isArray(raw)) {
    throw new Error('Expected an array of reviews');
  }
  return raw.map((item: unknown) => {
    const r = item as Record<string, unknown>;
    if (typeof r['id'] !== 'string' || typeof r['rating'] !== 'number' || typeof r['postedAt'] !== 'string') {
      throw new Error('Malformed review');
    }
    return { id: r['id'], rating: r['rating'], postedAt: new Date(r['postedAt']) };
  });
}

@Component({
  selector: 'app-product-reviews',
  template: `
    @if (reviews.error()) {
      <p>Reviews could not be loaded.</p>
    } @else {
      <p>{{ reviews.value().length }} reviews, {{ csvLines.value()?.length ?? 0 }} CSV lines</p>
    }
  `,
})
export class ProductReviews {
  readonly productId = input.required<string>();

  // value() is Review[]: inferred from parseReviews, never undefined thanks to defaultValue.
  readonly reviews = httpResource(() => `/api/products/${this.productId()}/reviews`, {
    parse: parseReviews,
    defaultValue: [],
  });

  // parse receives a string for the text() variant.
  readonly csvLines = httpResource.text(() => `/api/products/${this.productId()}/reviews.csv`, {
    parse: (csv) => csv.split('\n').filter((line) => line.length > 0),
  });
}

go deeper

for a junior

Know that the type argument is not a runtime check and that parse is where validation and conversion happen.

for a middle

Explain what parse receives for the JSON, text, blob and arrayBuffer forms, how its return type becomes the value type, and what a throw does.

for a senior

Use parse as the contract boundary, separate parse failures from HTTP failures in the UI and monitoring, and keep it consistent with SSR-cached responses.

for a principal

Decide on one validation approach for API boundaries across the app, weighing bundle size and CPU cost against catching contract drift early.

## Two ways to type a resource `httpResource` is generic in its result type. There are two ways to fill it. **The type argument.** `httpResource<Review[]>(() => url)` tells TypeScript to treat the body as `Review[]`. Nothing checks it at runtime; the parsed JSON is passed through as-is. If the backend renames `rating` to `score`, the template silently renders `undefined`. **The `parse` option.** `httpResource(() => url, { parse: parseReviews })` hands the body to your function first. The option is declared as `parse?: (value: TRaw) => TResult`: `TRaw` is what the HTTP layer produced, and `TResult` — the type of `value()` — is **inferred from the function's return type**, so no type argument is needed. ## What `parse` receives The raw type depends on which `httpResource` function you call: | Function | Body read as | `parse` receives | Default `value()` type | |---|---|---|---| | `httpResource(...)` | JSON | `unknown` | `unknown` (or the type argument) | | `httpResource.text(...)` | text | `string` | `string` | | `httpResource.blob(...)` | `Blob` | `Blob` | `Blob` | | `httpResource.arrayBuffer(...)` | `ArrayBuffer` | `ArrayBuffer` | `ArrayBuffer` | For JSON, receiving `unknown` is the point: the compiler forces you to check before you use the data. ## What `parse` is for 1. **Validation.** A hand-written type guard, or the `parse` function of a schema-validation library, which the Angular guide shows plugged in directly as `parse: schema.parse`. 2. **Transformation.** Build the shape the component wants: `Date` objects from ISO strings, numbers from numeric strings, a map keyed by id, lines of a CSV from `httpResource.text()`. 3. **Narrowing the type** without a separate mapping step in the component. ## When `parse` throws In the v22 source, the resource calls `parse` inside a `try` when the `Response` event arrives. A thrown error becomes the resource's error: - `status()` becomes `'error'` and `error()` holds what `parse` threw (a non-`Error` value is wrapped in an `Error` whose `cause` is the original). - `value()` throws on read, like any resource error, so guard it. - `statusCode()` and `headers()` still describe the successful HTTP response, which is how you tell "the server failed" from "the server answered with the wrong shape". ## Server rendering and the transfer cache When an app uses server-side rendering with the HTTP transfer cache, a request made on the server can be reused in the browser instead of being sent again. `httpResource` reads that cached response when it is created and runs `parse` on it too, so the hydrated value has the same shape and checks as a live one. If `parse` fails on the cached body, Angular logs a warning in development and falls back to requesting the data again. ## An example across the variants ```ts readonly reviews = httpResource(() => `/api/products/${this.productId()}/reviews`, { parse: parseReviews, // (raw: unknown) => Review[] defaultValue: [], }); readonly reviewsCsvLines = httpResource.text(() => `/api/products/${this.productId()}/reviews.csv`, { parse: (csv) => csv.split('\n').filter((line) => line.length > 0), }); readonly productPhoto = httpResource.blob(() => `/api/products/${this.productId()}/photo`); ``` ## Why `parse` rather than a `computed` in the component A `computed(() => validate(this.reviews.value()))` looks equivalent but behaves worse. It re-runs whenever its inputs change, and when validation throws, the `computed` rethrows on **every read**, so the template breaks instead of showing an error branch. `parse` runs once per response, inside the resource, and turns a failure into the resource's own `error` status, which the template already handles. It also keeps the resource's type honest: `value()` is the validated type, not `unknown` or an unchecked assertion. ## Mistakes to avoid - Believing the type argument validates anything. - Writing `parse` that returns partial data on a mismatch instead of throwing, which hides contract breaks. - Doing heavy work in `parse` for large payloads on every refetch without measuring it. - Declaring `Date` fields on the raw type instead of converting in `parse`.

  • An httpResource with a parse function shows the error branch, yet the network panel shows 200 OK. How do you confirm what happened?
    Read `statusCode()` and `error()` together. A `statusCode()` of 200 with an error means the HTTP call succeeded and `parse` rejected the body, so `error()` holds the validation error rather than an `HttpErrorResponse`. Compare the actual payload with the expected schema; the fix is either the backend contract or the parser.
  • Why does parse receive unknown for the JSON form of httpResource rather than the type you expect?
    Because at that point nothing has checked the data: it is whatever `JSON.parse` produced from the server's bytes. Typing it as `unknown` forces the parser to narrow each field before use, so the only way to produce a typed `Review[]` is to have actually verified it.

saying these in an interview costs you the question

  • httpResource<Review[]>() validates the response against the Review type at runtime.
  • parse receives an already typed Review[] for the JSON form.
  • If parse throws, value() keeps the raw unparsed body.
  • A parse failure sets statusCode() to 0.
  • httpResource.text() cannot take a parse function.