Angular

HttpClient: запросы и типизация

Почти каждое приложение рано или поздно ходит за данными по HTTP. Голый fetch работает, но возвращает any, не интегрирован с DI, не даёт единого места для заголовков авторизации и обработки ошибок. HttpClient в Angular решает всё это: запрос типизируется дженериком и возвращает Observable, заголовки и ошибки централизуются через функциональные интерсепторы, а сам клиент инжектится как обычный сервис. При строгой типизации это даёт поток данных, где тип ответа известен компилятору от запроса до шаблона - без единого any и без приведений as.

  • provideHttpClient(): стандартный способ подключить HTTP-стек в standalone-приложении без HttpClientModule
  • Функциональные интерсепторы: один HttpInterceptorFn добавляет токен авторизации ко всем исходящим запросам
  • Типизированные ответы: get<User>('/api/user/1') возвращает Observable<User>, и компилятор знает форму данных
  • Централизованная обработка ошибок: интерсептор ловит 401 и редиректит на логин для всего приложения сразу
  • REST-интеграции: типизированные методы сервиса инкапсулируют URL и форму данных, а компоненты работают с чистыми типами

Предварительные знания

  • Сервисы Angular, providedIn: 'root' и функция inject()
  • Базовое представление об Observable и операторах RxJS (map, catchError)
  • Дженерики TypeScript и описание интерфейсов данных

Подключение и типизированные запросы

HTTP-стек подключается функцией provideHttpClient() в конфигурации приложения. После этого HttpClient доступен через inject() в любом сервисе. Методы запросов принимают дженерик типа ответа: этот параметр говорит компилятору, какой формы придут данные, и весь дальнейший поток остаётся типизированным.

Дженерик в get<User> не делает приведения. Это аннотация ожидаемого типа: компилятор начинает считать результат Observable<User> и проверяет всё, что строится дальше. Никакого as User не требуется, потому что тип задан в точке запроса, а не навязан постфактум кастом.

Дженерик метода - это обещание, а не проверка в рантайме: HttpClient не валидирует, что сервер действительно прислал User. Если форма ответа под сомнением, ответ берут как unknown и проверяют type guard или схемой (например, Zod), а не приведением as, которое лишь заглушает компилятор.

Что на самом деле делает дженерик в вызове http.get<User>(url)?

Observable и потребление в шаблоне

Методы HttpClient возвращают холодный Observable: сам по себе вызов get не отправляет запрос. Запрос уходит только при подписке. Это даёт контроль, но требует помнить про подписку и отписку. В шаблоне эту заботу снимает AsyncPipe: она подписывается при отображении и отписывается при уничтожении компонента.

AsyncPipe возвращает текущее значение Observable или null до прихода ответа. Тип user внутри @if сужается до User благодаря проверке. Это типобезопасный шаблон: компилятор знает, что после @if (user) поле name существует, и не требует приведений.

Каждая подписка на холодный Observable HttpClient запускает новый запрос. Если результат нужен в нескольких местах шаблона, повторное user$ | async создаст дубли запросов. Решение - @let user = user$ | async один раз или оператор shareReplay, чтобы переиспользовать один результат.

Метод сервиса вызвали: const obs = api.getOne(1). Запрос уже ушёл на сервер?

Интерсепторы и обработка ошибок

Функциональный интерсептор (HttpInterceptorFn) - это функция, через которую проходит каждый запрос перед отправкой и каждый ответ на обратном пути. Это единое место для сквозных задач: добавить заголовок авторизации, залогировать, обработать ошибку. Интерсепторы регистрируются списком в provideHttpClient и применяются по порядку.

Объект запроса иммутабелен, поэтому изменения вносят через req.clone. Интерсептор обязательно возвращает next(req) или next(модифицированный запрос), передавая управление дальше по цепочке. Каждый интерсептор видит и запрос, и ответный поток, что удобно для централизованной обработки ошибок.

В catchError ошибка приходит как unknown, а не как any. Сужать её надо через type guard instanceof HttpErrorResponse, а не приведением as HttpErrorResponse. Каст заглушил бы компилятор и пропустил бы случай, когда прилетела не HTTP-ошибка, а, например, исключение из оператора выше по цепочке.

В операторе catchError ошибка имеет тип unknown. Как типобезопасно получить из неё статус-код HTTP?

Связь с другими темами

Урок про получение данных по HTTP. Дальше эти данные заводят в реактивный мир сигналов:

  • Resource API и httpResource — httpResource оборачивает HttpClient и отдаёт результат как сигналы value/status/error
  • InjectionToken и провайдеры — Интерсепторы регистрируются как функциональные провайдеры, base URL приходит через конфигурационный токен

Итог

  • provideHttpClient() подключает HTTP-стек в ApplicationConfig, без HttpClientModule
  • Методы get/post/put/delete принимают дженерик ответа: get<User>(url) возвращает Observable<User>, тип известен компилятору
  • Результат - холодный Observable: запрос уходит только при подписке, AsyncPipe подписывается и отписывается сам
  • Функциональные интерсепторы (HttpInterceptorFn) централизуют заголовки, логирование и обработку ошибок для всех запросов
  • Строгая типизация без any и as достигается дженериками методов и сужением unknown в catchError через type guard

Связанные уроки

  • ng-27-resource-httpresource — httpResource построен поверх HttpClient и заводит его результат в сигналы, поэтому базовый HttpClient идёт первым
  • ng-18-services — Сервисы и inject() - то, во что оборачивают HttpClient и где описывают типизированные методы запросов
HttpClient: запросы и типизация

0

1

Войти