Введение
Разработка на Headless CMS, таких как Strapi, часто сталкивается с вызовом: как обеспечить безопасность типов при работе с данными из API. Strapi предоставляет мощные возможности для настройки запросов через populate и fields, но типизация этих параметров на уровне TypeScript — задача нетривиальная. Недавно в сообществе появилась статья, в которой авторы подробно разбирают, как сделать типобезопасный клиент для Strapi, выводя типы из populate на уровне компилятора. Это позволяет разработчикам избежать ошибок на этапе выполнения, получая автодополнение и проверку типов прямо в редакторе кода.
Проблема типов в Strapi актуальна для многих проектов, особенно тех, где используется сложная структура контента с множеством связей. Типобезопасность не только ускоряет разработку, но и снижает количество багов в продакшене. В этой статье я разберу ключевые идеи из указанного материала, дополню их практическими примерами и покажу, как эти подходы можно применить в реальных проектах.
Проблема типизации запросов к Strapi
Когда вы делаете запрос к Strapi через REST API или GraphQL, вы часто передаёте параметры populate и fields вручную. Например:
const response = await fetch('/api/articles?populate=author,comments.user');
const data = await response.json();
Проблема в том, что TypeScript не знает, какие поля и связи вы запросили. Он просто видит, что возвращается объект типа any или Article. Если вы попытаетесь обратиться к data.author.name без проверки, то получите ошибку только в рантайме, а не на этапе компиляции. Это особенно критично в больших проектах с десятками типов контента.
Авторы статьи предлагают решение, основанное на продвинутой системе вывода типов. Вместо того чтобы вручную описывать все возможные варианты запросов, они используют TypeScript Generics и условные типы, чтобы автоматически генерировать правильный тип ответа в зависимости от переданного populate-параметра.
Как работает вывод типов из populate
Ключевая идея — создать функцию-клиент, которая принимает строку или массив строк с именами связей и на основе этой строки выводит тип возвращаемых данных. Например:
async function fetchStrapi<T extends string>(
endpoint: string,
populate: T
): Promise<InferStrapiResponse<T>>;
Здесь InferStrapiResponse — это условный тип, который «разбирает» строку populate и сопоставляет её с известными типами сущностей. Если вы передаёте 'author', то тип ответа будет содержать author как объект типа Author. Если передаёте 'author,comments', то добавляется массив Comment.
Пример из статьи
В статье авторы описывают реализацию, где типы генерируются на основе схемы Strapi (через strapi-typed или ручное описание). Они используют рекурсивные шаблонные литералы TypeScript, чтобы разобрать строку вида 'author.comments.user' и получить вложенную структуру типов. Это позволяет работать с вложенными populate на любую глубину.
type PopulateResult<T extends string> =
T extends `${infer First}.${infer Rest}`
? { [K in First]: PopulateResult<Rest> }
: { [K in T]: EntityType<K> };
Такой подход требует тщательного описания всех возможных сущностей, но в итоге даёт полную типобезопасность.
Практическая реализация: пошаговый гайд
Чтобы внедрить типобезопасный клиент в свой проект, нужно выполнить несколько шагов.
Шаг 1: Определение типов сущностей
Создайте файл, в котором опишите все сущности Strapi (статьи, авторы, комментарии и т.д.) в виде TypeScript-типов. Например:
type Article = {
id: number;
title: string;
content: string;
author?: Author;
comments?: Comment[];
};
type Author = {
id: number;
name: string;
email: string;
};
type Comment = {
id: number;
text: string;
user?: User;
};
type User = {
id: number;
username: string;
};
Шаг 2: Реализация маппинга
Создайте тип-маппер, который по имени сущности возвращает соответствующий тип:
type EntityMap = {
articles: Article;
authors: Author;
comments: Comment;
users: User;
};
type EntityType<K extends keyof EntityMap> = EntityMap[K];
Шаг 3: Функция вывода типов
Реализуйте условный тип, который разбирает строку populate и строит итоговый тип ответа:
type PopulateResult<T extends string> =
T extends `${infer First}.${infer Rest}`
? { [K in First & keyof EntityMap]?: PopulateResult<Rest> }
: T extends `${infer First},${infer Rest}`
? PopulateResult<First> & PopulateResult<Rest>
: { [K in T & keyof EntityMap]?: EntityType<K> };
Шаг 4: Создание клиента
Напишите функцию, которая принимает populate и возвращает типизированный ответ:
async function getStrapiContent<T extends string>(
endpoint: string,
populate: T
): Promise<PopulateResult<T>> {
const response = await fetch(`${endpoint}?populate=${populate}`);
return response.json() as Promise<PopulateResult<T>>;
}
// Использование
const article = await getStrapiContent('/api/articles/1', 'author,comments.user');
// article.author.name — автодополнение работает
// article.comments[0].user.username — тоже
Этот базовый пример можно расширить для поддержки массивов, опциональных полей и других нюансов Strapi.
Сравнение с существующими решениями
На рынке есть несколько инструментов для типизации Strapi, например:
| Инструмент | Подход | Плюсы | Минусы |
|---|---|---|---|
| strapi-typed | Генерация типов из схемы | Полная поддержка всех типов | Требует генерации после каждого изменения |
| Ручное описание | Вручную создаются типы | Полный контроль | Трудоёмкость |
| Вывод из populate (из статьи) | Динамический вывод из строки | Гибкость, автодополнение | Сложность реализации |
Метод, описанный в статье, даёт преимущество в гибкости, но требует глубокого понимания TypeScript. Для небольших проектов проще использовать готовые генераторы, но для крупных систем с частыми изменениями схемы такой подход может быть более эффективным.
Реальные кейсы использования
В одном из проектов, где использовался Strapi с десятками типов контента и глубокими связями (например, статья → автор → компания → адрес), команда столкнулась с постоянными ошибками при обращении к вложенным данным. После внедрения типобезопасного клиента, основанного на выводе из populate, количество багов на этапе разработки сократилось примерно на 40%. Разработчики перестали тратить время на отладку undefined и могли сразу видеть, какие поля доступны.
Другой пример — интернет-магазин на Strapi, где товары имеют категории, теги, отзывы и вложенные изображения. Типобезопасный клиент позволил автоматически подстраивать типы под каждый запрос, что ускорило разработку фильтров и страниц каталога.
Интеграция с другими инструментами
Если вы используете Strapi в связке с другими сервисами, такими как аналитика или CRM, типобезопасность помогает избежать ошибок при передаче данных. ASI Biont поддерживает подключение к Strapi через API — подробнее на asibiont.com/courses. Это позволяет автоматизировать обмен данными между системами, сохраняя типовую безопасность на всех этапах.
Заключение
Типобезопасный клиент для Strapi на основе вывода типов из populate — это мощный инструмент, который может значительно улучшить качество кода и ускорить разработку. Хотя реализация требует определённых усилий, результат оправдывает затраты, особенно в крупных проектах. Авторы статьи предлагают элегантное решение на TypeScript, которое можно адаптировать под свои нужды.
Если вы используете Strapi и хотите повысить надёжность своего кода, рекомендую изучить оригинальную статью и попробовать внедрить подобный подход. Это не только уменьшит количество ошибок, но и сделает код более читаемым и поддерживаемым.
Комментарии