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()?
answer
- a generic is only a promise
- the raw value arrives as unknown
- the return type becomes the value type
- a throw becomes the error state
basics
~20 shttpResource<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 linesimport { 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
Know that the type argument is not a runtime check and that parse is where validation and conversion happen.
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.
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.
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.