Типизация API-ответов
Типизация API-ответов — набор паттернов TypeScript для безопасной работы с данными от HTTP-запросов: от простых интерфейсов до runtime-валидации через zod, с сохранением типобезопасности на всём пути данных.
Зачем нужно
API возвращает any (результат response.json). Без явной типизации весь код, работающий с ответом, лишается проверок. Правильная типизация API-ответов ловит несоответствия между ожидаемым и фактическим форматом данных.
Где используется
- Fetch/axios запросы к REST API
- GraphQL-клиенты
- Обработка WebSocket-сообщений
- Чтение данных из localStorage/sessionStorage
- Обработка webhook-нотификаций
Основной контент
Простейший вариант: type assertion
interface User {
id: number;
name: string;
email: string;
}
async function getUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<User>; // простой cast — без runtime-проверки
}
const user = await getUser(1);
console.log(user.name); // TypeScript: string — но реальная проверка отсутствует
Generic fetch-обёртка
async function fetchApi<T>(
url: string,
options?: RequestInit
): Promise<T> {
const res = await fetch(url, options);
if (!res.ok) {
const error = await res.text();
throw new Error(`${res.status}: ${error}`);
}
return res.json() as Promise<T>;
}
// Использование
const users = await fetchApi<User>("/api/users");
const post = await fetchApi<Post>("/api/posts/1");
Паттерн ApiResponse с метаданными
interface ApiResponse<T> {
data: T;
meta: {
total: number;
page: number;
perPage: number;
};
status: "success" | "error";
}
interface ErrorResponse {
status: "error";
message: string;
code: string;
}
type Response<T> = ApiResponse<T> | ErrorResponse;
async function fetchUsers: Promise<User> {
const res = await fetchApi<ApiResponse<User>>("/api/users");
return res.data;
}
Runtime-валидация через zod
import { z } from "zod";
const UserSchema = z.object({
id: z.number,
name: z.string,
email: z.string.email,
role: z.enum(["admin", "user"]),
});
type User = z.infer<typeof UserSchema>; // тип выводится из схемы!
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const raw = await res.json();
return UserSchema.parse(raw); // кидает ZodError если данные не совпадают
}
// Безопасный вариант (не кидает, возвращает Result):
async function fetchUserSafe(id: number): Promise<User | null> {
const raw = await fetch(`/api/users/${id}`).then(r => r.json());
const result = UserSchema.safeParse(raw);
return result.success ? result.data : null;
}
Пользовательский type guard для API
function isApiError(response: unknown): response is { error: string } {
return (
typeof response === "object" &&
response !== null &&
"error" in response &&
typeof (response as { error: unknown }).error === "string"
);
}
async function fetchData<T>(url: string): Promise<T> {
const res = await fetch(url);
const json: unknown = await res.json();
if (!res.ok) {
if (isApiError(json)) {
throw new Error(json.error);
}
throw new Error(`HTTP ${res.status}`);
}
return json as T;
}
Типизация axios с перехватчиками
import axios from "axios";
const api = axios.create({ baseURL: "/api" });
// Типизация ответа через generic
const { data: user } = await api.get<User>("/users/1");
// data: User
// Interceptor с типизацией
api.interceptors.response.use(
(response) => response,
(error: unknown) => {
if (axios.isAxiosError(error) && error.response) {
const status = error.response.status;
if (status === 401) { /* redirect to login */ }
}
return Promise.reject(error);
}
);
Частые ошибки
- Доверять
as Tбез runtime-проверки — cast не проверяет реальный формат данных; API может вернуть неожиданную структуру. - Не обрабатывать ошибки HTTP —
fetchне кидает исключение при 4xx/5xx; нужно проверятьres.ok. - Типизировать вложенные ответы вручную — при изменении API нужно обновлять все типы; zod автоматически синхронизирует тип и схему.
- Игнорировать
nullполя — API часто возвращаетnullдля опциональных полей; тип должен отражать это.
Связанные темы
- Awaited -- тип для промисов
- Пользовательские Type Guards
- Generic функции
- any, unknown -- различия
- _MOC TypeScript