skip to content

An Angular WeatherService uses http.get<Forecast>() and compiles, yet forecast.issuedAt.getHours() throws in production; why did the type not catch it, and how do you harden it?

level: seniorimportance: should knowfreq 38%

answer

  1. compile time versus runtime
  2. what JSON.parse can produce
  3. the generic is an assertion
  4. validate where data enters

basics

~20 s

The generic in http.get<Forecast>() is an unchecked assertion: HttpClient parses the JSON and passes it on without checking it, and JSON has no Date. Type the wire format honestly, validate it in the service, and map it to domain types there.

solid answer

~40 s

`get<Forecast>()` only tells the compiler what to assume; the Angular guide calls the generic a type **assertion**, and for `responseType: 'json'` `HttpClient` runs `JSON.parse` and passes the result through unchecked. JSON has no `Date`, so `issuedAt` is really a string and `getHours()` throws. The same gap hides renamed or missing fields, `null` bodies from a 204, and numbers sent as strings. To harden it, type the response as the **wire DTO** (`issuedAt: string`) or as `unknown`, validate it in the service with a type guard or a schema validator, and convert to the domain model there — so components only ever see checked `Forecast` objects. Validation failures then surface as an explicit error on the stream, and generating DTO types from the API contract keeps both sides aligned.

code

ts · 47 lines
ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, map } from 'rxjs';

// Wire format: exactly what JSON can carry.
interface ForecastDto {
  city: string;
  tempC: number;
  issuedAt: string;
}

// Domain model used by components.
export interface Forecast {
  city: string;
  tempC: number;
  issuedAt: Date;
}

export class ForecastContractError extends Error {}

function isForecastDto(value: unknown): value is ForecastDto {
  if (typeof value !== 'object' || value === null) {
    return false;
  }
  const v = value as Record<string, unknown>;
  return typeof v['city'] === 'string' && typeof v['tempC'] === 'number' && typeof v['issuedAt'] === 'string';
}

@Injectable({ providedIn: 'root' })
export class WeatherService {
  private readonly http = inject(HttpClient);

  getForecast(city: string): Observable<Forecast> {
    return this.http.get<unknown>('/api/forecast', { params: { city } }).pipe(
      map((raw) => {
        if (!isForecastDto(raw)) {
          throw new ForecastContractError(`Unexpected forecast payload for ${city}`);
        }
        const issuedAt = new Date(raw.issuedAt);
        if (Number.isNaN(issuedAt.getTime())) {
          throw new ForecastContractError(`Invalid issuedAt for ${city}: ${raw.issuedAt}`);
        }
        return { city: raw.city, tempC: raw.tempC, issuedAt };
      }),
    );
  }
}

go deeper

for a junior

Remember that the type argument on http.get<T>() is a compile-time label, and that JSON dates arrive as strings.

for a middle

Explain why no runtime check happens for JSON, and show a DTO-to-domain mapping in the service that parses dates and handles null bodies.

for a senior

Put validation at the service boundary, separate contract errors from HTTP errors in handling and monitoring, and choose validation depth by how much you trust the API and payload size.

for a principal

Decide the contract strategy across teams: generated clients from an API description, contract tests in CI, and which boundaries require runtime validation.

## Why the compiler was satisfied In TypeScript, a **type argument** such as `Forecast` in `http.get<Forecast>()` exists only at compile time. `HttpClient`'s JSON overload is declared as `get<T>(url, options?): Observable<T>`, and the Angular guide is explicit: the generic is a **type assertion** about what the server returns; `HttpClient` does not verify that the data matches. At runtime the default `responseType: 'json'` path does this: 1. The backend receives the bytes and runs `JSON.parse` on them (an empty body becomes `null`). 2. The result is emitted as the body. In the v22 source the JSON branch is a plain `map((res) => res.body)` with the comment that no validation is needed because JSON can be any type. So `T` is a promise you made to the compiler, not one the server made to you. The failing code looks harmless: ```ts interface Forecast { city: string; tempC: number; issuedAt: Date; } this.http.get<Forecast>('/api/forecast', { params: { city } }).subscribe((f) => { this.issuedHour = f.issuedAt.getHours(); // TypeError: getHours is not a function }); ``` The compiler accepted `f.issuedAt.getHours()` because it believed the assertion; at runtime `issuedAt` is the string the server sent. ## What JSON can and cannot give you | Declared in `Forecast` | What actually arrives | Failure | |---|---|---| | `issuedAt: Date` | an ISO string | `issuedAt.getHours is not a function` | | `tempC: number` | `"12.5"` from a lenient backend | string concatenation instead of maths | | `summary: string` (renamed server-side) | `undefined` | blank UI or a crash deep in a template | | a class with methods | a plain object | methods are missing | | `Forecast` from a 204 | `null` | property access on `null` | None of these fail where the data enters; they fail later, far from the cause, which is why they reach production. ## Hardening the boundary The rule is to check data **once, where it enters the app** — in the data service — and let the rest of the code trust the domain type. 1. **Type the wire format honestly.** Declare a `ForecastDto` with `issuedAt: string`, or request `get<unknown>()` so the compiler forces you to check before use. 2. **Validate.** A hand-written type guard, or a schema-validation library, confirms required fields and their types. A failed check throws a descriptive error inside `map`, so it travels down the Observable's error channel like any other failure. 3. **Convert.** Build the domain `Forecast` from the validated DTO: parse the date, rename fields, apply defaults. Components never see the DTO. 4. **Keep types in sync with the contract.** Generating DTO types from an OpenAPI description, plus contract tests in CI, catches drift before users do. Note that a validation error thrown in `map` is your own error class, not an `HttpErrorResponse`: the HTTP exchange succeeded. Error handling that branches on `instanceof HttpErrorResponse` should treat it as a separate case, and it deserves its own monitoring signal because it means the API contract broke. ## Choosing how much to validate - **Third-party or public APIs**: validate fully; you do not control their releases. - **Your own backend deployed together with the frontend**: generated types plus contract tests may be enough, with runtime checks on the few fields whose failure would corrupt state. - **Large payloads**: validating thousands of rows on every call has a CPU cost; validate the shape of a sample or the top level, or validate once when data is cached. - **Signal-based fetching**: `httpResource()` offers a `parse` option for the same job; that API is covered with signal-driven fetching. ## Anti-patterns - Casting with `as Forecast` inside the component to silence the compiler. - Declaring `Date` fields on response interfaces. - Letting components each re-check the same fields defensively instead of fixing the boundary. - Swallowing validation errors with an empty fallback, which hides a broken contract from monitoring.

  • In that weather service, how should a component tell a contract violation apart from an HTTP failure?
    A validation failure thrown in `map` is your own error class, such as `ForecastContractError`, not an `HttpErrorResponse`, because the HTTP exchange itself succeeded. Handlers can branch with `instanceof`: `HttpErrorResponse` means transport or server status problems, the contract error means the API changed shape. Report the latter to monitoring distinctly, since retrying will not fix it.
  • What does http.get() return when you omit the type argument entirely?
    With no type argument the JSON overload types the body as `Object`, which lets almost nothing through without a cast. The Angular guide suggests `unknown` for data of uncertain structure instead, because `unknown` forces a check or narrowing before any property is used, which is exactly the validation step you want.

The type argument is the label on a delivery crate. HttpClient reads the label and hands the crate on without opening it; only an inspection at the loading dock, your validation step, finds out that the crate marked Forecast actually holds something else.

saying these in an interview costs you the question

  • HttpClient checks the JSON body against the generic type and errors on a mismatch.
  • Declaring a Date field makes HttpClient convert ISO strings to Date objects.
  • Strict TypeScript settings make http.get<T>() type-safe at runtime.
  • A validation error thrown in map arrives as an HttpErrorResponse.
  • Casting with as Forecast in the component is an adequate fix.