Коротко: Руководство по строгой типизации сетевых моков в MSW v2 с TypeScript: анатомия дженериков, параметры путей, типизация DTO и обработка ошибок.
Ситуация, когда автотесты остаются зелеными, а приложение падает в продакшене из-за изменившейся схемы ответа бэкенда, знакома большинству фронтенд-инженеров. Причина кроется в рассинхронизации контрактов: моки в тестах и локальной разработке описываются вручную как произвольные JSON-объекты без проверки соответствия реальным типам API.
Mock Service Worker (MSW) версии 2.x кардинально переработал подход к мокированию, отказавшись от собственных абстракций в пользу веб-стандартов (Fetch API) и глубокой интеграции с TypeScript. Строгая типизация сетевого слоя позволяет валидировать параметры маршрута, тело запроса и структуру ответа непосредственно на этапе компиляции.
Что изменилось в типизации при переходе на MSW v2
В первой версии библиотеки моки строились вокруг объекта rest и фабрики ответа res(ctx.status(), ctx.json()). Такой подход изолировал логику моков от стандартного браузерного окружения и требовал специфических интерфейсов для типизации контекста.
Отказ от res/ctx в пользу веб-стандарта HttpResponse
В MSW v2 архитектура перехвата запросов опирается на нативный Fetch API: интерфейсы Request и Response. Для формирования ответов используется класс HttpResponse, расширяющий стандартный Response.
// Было в MSW v1:
// rest.get('/api/user', (req, res, ctx) => res(ctx.json({ id: '1' })))
// Стало в MSW v2:
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/user', () => {
return HttpResponse.json({ id: '1' });
}),
];
Благодаря переходу на стандартные объекты устраняется дублирование типов между тестовым и рабочим кодом.
Системные требования: TypeScript 4.7+ и нативная поддержка ESM
Для полноценного вывода типов в MSW v2 требуется TypeScript версии не ниже 4.7. Это обусловлено использованием современных возможностей резолвинга модулей (moduleResolution: "node16" или "nodenext"), поддержкой ESM и расширенными механизмами сужения generic-типов в сигнатурах функций.
Анатомия дженериков в HTTP-хендлерах MSW v2
Основа типобезопасности в MSW v2 — generic-параметры методов http.get, http.post, http.put, http.delete и других обработчиков.
Разбор сигнатуры: Params, RequestBodyType, ResponseBodyType, Path
Полная сигнатура метода HTTP-хендлера выглядит следующим образом:
http.method<
Params extends PathParams,
RequestBodyType extends DefaultBodyType,
ResponseBodyType extends DefaultBodyType,
Path extends string
>(path, resolver)
Params(PathParams) — типизирует именованные параметры URL-маршрута (например,:id,:slug).RequestBodyType(DefaultBodyType) — задает ожидаемый тип данных дляawait request.json().ResponseBodyType(DefaultBodyType) — строго контролирует схему данных, возвращаемую черезHttpResponse.json().Path(string) — строковый литерал пути, позволяющий TypeScript контролировать соответствие переданного URL шаблону.
Типизация параметров пути (PathParams)
Если маршрут содержит динамические сегменты, параметры доступны внутри резолвера через свойство params. Передача интерфейса в первый дженерик гарантирует доступ к полям с корректными типами:
import { http, HttpResponse, PathParams } from 'msw';
interface UserPathParams extends PathParams {
userId: string;
}
interface UserDTO {
id: string;
name: string;
}
export const getUserHandler = http.get<UserPathParams, never, UserDTO | { error: string }>(
'/api/users/:userId',
({ params }) => {
const { userId } = params; // Тип: string
if (userId === '404') {
return HttpResponse.json({ error: 'Пользователь не найден' }, { status: 404 });
}
return HttpResponse.json({
id: userId,
name: 'Алексей Иванов',
});
}
);
Строгая проверка тела входящего запроса (request.json())
При обработке мутаций (POST, PUT, PATCH) компилятор TypeScript защищает от обращения к несуществующим полям в теле входящего запроса.
import { http, HttpResponse } from 'msw';
interface CreateArticlePayload {
title: string;
content: string;
tags: string[];
}
interface ArticleDTO extends CreateArticlePayload {
id: string;
createdAt: string;
}
export const createArticleHandler = http.post<
never,
CreateArticlePayload,
ArticleDTO
>(
'/api/articles',
async ({ request }) => {
const body = await request.json();
// TypeScript знает структуру body: title, content, tags
const newArticle: ArticleDTO = {
id: 'art-101',
title: body.title,
content: body.content,
tags: body.tags,
createdAt: new Date().toISOString(),
};
return HttpResponse.json(newArticle, { status: 201 });
}
);
Контроль структуры ответа через HttpResponse.json()
Дженерик ResponseBodyType не позволяет вернуть объект, нарушающий объявленный контракт. Если в возвращаемом объекте пропущено обязательное свойство или указан неверный тип данных, TypeScript выбросит ошибку на этапе сборки.
interface DashboardStatsDTO {
views: number;
conversions: number;
}
http.get<never, never, DashboardStatsDTO>(
'/api/stats',
() => {
// Ошибка компиляции: свойство 'conversions' пропущено
// return HttpResponse.json({ views: 1200 });
return HttpResponse.json({
views: 1200,
conversions: 45,
});
}
);
Практические паттерны: от простых GET-запросов до сложных сценариев
Интеграция с общими DTO и сгенерированными OpenAPI схемами
Наибольшая эффективность строгой типизации достигается при использовании общих типов между бэкендом и фронтендом (через Monorepo или генераторы вроде openapi-typescript).
// schemas/api.ts (сгенерировано из OpenAPI)
export interface components {
schemas: {
Product: {
id: number;
title: string;
price: number;
};
UpdateProductPayload: {
title?: string;
price?: number;
};
};
}
// handlers.ts
import { http, HttpResponse, PathParams } from 'msw';
type Product = components['schemas']['Product'];
type UpdateProductPayload = components['schemas']['UpdateProductPayload'];
interface ProductParams extends PathParams {
id: string;
}
export const updateProductHandler = http.patch<
ProductParams,
UpdateProductPayload,
Product
>(
'/api/products/:id',
async ({ params, request }) => {
const payload = await request.json();
return HttpResponse.json({
id: Number(params.id),
title: payload.title ?? 'Дефолтное название',
price: payload.price ?? 0,
});
}
);
Мокирование ошибок: типизация Error Payload и HttpResponse.error()
Сетевые сбои делятся на два типа: сбои транспортного уровня (network error, CORS failure) и HTTP-ошибки с телом ответа (4xx, 5xx).
Для имитации полного обрыва соединения в MSW v2 используется статический метод HttpResponse.error():
export const networkFailureHandler = http.get('/api/health', () => {
return HttpResponse.error();
});
Для типизированных HTTP-ошибок формируется объединение типов (union type) в ResponseBodyType:
interface ErrorResponse {
code: string;
message: string;
}
http.post<never, CreateArticlePayload, ArticleDTO | ErrorResponse>(
'/api/articles',
async ({ request }) => {
const data = await request.json();
if (!data.title) {
return HttpResponse.json(
{ code: 'VALIDATION_ERROR', message: 'Поле title обязательно' },
{ status: 400 }
);
}
return HttpResponse.json({
id: '1',
title: data.title,
content: data.content,
tags: data.tags,
createdAt: '2025-01-01',
});
}
);
Работа со стримами (ReadableStream) и FormData
MSW v2 поддерживает потоковую передачу данных и работу с бинарными и составными телами запросов на уровне стандартов Web Streams API.
export const sseStreamHandler = http.get('/api/events', () => {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode('data: {"status":"processing"}\n\n'));
controller.enqueue(encoder.encode('data: {"status":"completed"}\n\n'));
controller.close();
},
});
return new HttpResponse(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
},
});
});
Типичные ошибки типизации в MSW v2 и как их избежать
Неявное приведение к any при парсинге тела
Если опустить generic-параметр RequestBodyType, вызов await request.json() возвращает тип unknown (или any в зависимости от настроек компилятора). Прямой кастинг через as MyType отключает проверки и скрывает потенциальные ошибки:
// ❌ Небезопасное приведение:
http.post('/api/item', async ({ request }) => {
const body = (await request.json()) as ItemPayload;
return HttpResponse.json(body);
});
// ✅ Строгий дженерик:
http.post<never, ItemPayload, ItemDTO>('/api/item', async ({ request }) => {
const body = await request.json(); // Тип: ItemPayload
return HttpResponse.json({ id: 'new-id', ...body });
});
Расхождение сигнатур параметров путей с шаблоном маршрута
Если в строке пути указан параметр :orderId, а в интерфейсе Params объявлено поле id, компилятор не сможет сопоставить их автоматически, если тип пути не сужен. Рекомендуется строго синхронизировать ключи интерфейса параметров с именами плейсхолдеров в URL:
interface OrderParams extends PathParams {
orderId: string;
}
// Путь строго содержит :orderId
http.get<OrderParams, never, OrderDTO>('/api/orders/:orderId', ({ params }) => {
const id = params.orderId;
return HttpResponse.json({ id, total: 1000 });
});
FAQ
Какая минимальная версия TypeScript требуется для работы с дженериками в MSW v2?
Для корректной работы системы вывода типов MSW v2 необходим TypeScript 4.7 или выше. Рекомендуется использовать актуальные версии TypeScript (5.x) с включенным строгим режимом ("strict": true в tsconfig.json).
Как в MSW v2 типизировать ответ с сетевой ошибкой?
Для симуляции аппаратного сбоя сети или блокировки CORS используется метод HttpResponse.error(). Для возврата серверных ошибок со специфическим телом (400, 404, 500) используется объединение типов в ResponseBodyType и вызов HttpResponse.json(errorBody, { status: 500 }).
Нужно ли вручную указывать все 4 дженерика в http.get или http.post?
Нет, если параметры пути или тело запроса отсутствуют, можно передавать never или использовать дефолтные значения. Также TypeScript выводит тип пути Path автоматически из переданного первого аргумента функции.
Как переиспользовать сгенерированные типы OpenAPI/Swagger в хендлерах MSW?
Схемы запросов и ответов из сгенерированных определений OpenAPI подставляются напрямую в качестве второго (RequestBodyType) и третьего (ResponseBodyType) generic-аргументов. Это гарантирует совпадение моков с документацией API.
Как типизировать ответ, если эндпоинт возвращает пустой ответ (204 No Content)?
Для ответов без тела в качестве ResponseBodyType указывается null или never, а хендлер возвращает новый экземпляр new HttpResponse(null, { status: 204 }).
Чем отличается HttpResponse.error() от возврата статуса 500?
HttpResponse.error() моделирует системный отказ сети (Network Error), при котором промис fetch() отклоняется с исключением TypeError. Ответ со статусом 500 через HttpResponse.json() успешно завершает вызов fetch(), но возвращает объект ответа со свойством response.ok === false.
Заключение и чек-лист внедрения
Строгая типизация моков с помощью MSW v2 превращает тесты из потенциального источника ложной уверенности в надежный инструмент контроля контрактов.
Чек-лист по организации типобезопасного мокирования:
- Обновите TypeScript в проекте минимум до версии 4.7 (рекомендуется 5.x).
- Замените устаревшие вызовы
rest.*иres(ctx.*)наhttp.*иHttpResponse.*. - Подключите единый источник правды для типов (OpenAPI-схемы или общие DTO из монорепозитория).
- Описывайте дженерики
Params,RequestBodyTypeиResponseBodyTypeдля всех хендлеров с динамическими данными. - Избегайте ручного приведения типов через
asвнутри функций-резолверов.




.svg.webp)





