Глубокая типизация GraphQL-ответов без codegen: generics и conditional types в TypeScript
Глубокая типизация GraphQL-ответов без codegen: generics и conditional types в TypeScript Codegen для GraphQL часто превращается в головную боль: настройка схемы, синхронизация версий, регенерация при каждом изменении. Но если вы используете Relay с фрагментами или работаете в микросервисе с часто меняющейся схемой, можно обойтись без codegen. Типичная ошибка — вручную писать типы для каждого запроса, что для вложенных структур с пагинацией превращается в копипасту. Почему codegen не всегда нужен Codegen добавляет слой абстракции, который требует CI/CD-интеграции и синхронизации с серверной схемой. В проектах с частыми изменениями или фрагментарной архитектурой Relay это может стать узким местом. Альтернатива — вывести типы из самого запроса через TypeScript. Подход: generic + conditional type Создаем маппер типов GraphQL на TypeScript и utility type, который рекурсивно "проходит" по структуре запроса, обнаруженной через as const. Этот тип заменяет строки на реальные типы из схемы, обрабатывая вложенность и массивы. // Маппер типов схемы type GraphQLTypeMap = { ID: string; String: string; User: { id: string; name: string; posts: Post[] }; Post: { id: string; title: string; comments: Comment[] }; Comment: { id: string; text: string; author: User }; }; // Рекурсивный conditional type type UnwrapGraphQLResponse<T> = T extends { __typename: infer Name } ? T & GraphQLTypeMap[Name extends keyof GraphQLTypeMap ? Name : never] : T extends (infer U)[] ? UnwrapGraphQLResponse<U>[] : T; // Запрос с as const const query = { user: { id: true, name: true, posts: { title: true, comments: { text: true, author: { name: true } } } } } as const; // Тип ответа выводится автоматически type Response = UnwrapGraphQLResponse<typeof query>; Акцент на trade-offs Главный плюс — один дженерик на все запросы без codegen. Но это требует ручного поддержания GraphQLTypeMap, что для схем с 50+ типами становится трудоемким. Для union/interface типов conditional type может дать сбой — понадобится доплогика. Также метод не работает с динамическими selection set. Типичные ошибки * Забыть указать as const — без него TypeScript не зафиксирует структуру запроса. * Использовать при сложных интерфейсах: conditional type не всегда корректно обрабатывает discriminated unions — это потребует дополнительных проверок. * Не синхронизировать маппер: если схема меняется, типы рассинхронизируются, и вы получите ложные гарантии. Практический совет Применяйте этот подход для микросервисов с простой, стабильной схемой или когда codegen конфликтует с Relay-фрагментарной структурой. В production я так типизировал запросы в проекте, где схема менялась раз в неделю, а codegen падал из-за кэширования — это сэкономило время без потери безопасности типов. Вывод: Generics и conditional types — это легковесная альтернатива codegen для типизации вложенных GraphQL-ответов, но она требует ручного управления маппером и подходит для схем ограниченного размера.