Магия декларативности и схемы. Часть 4: Кодогенерация
Напоминалка про пример
Вы уже добрались до четвертого поста и наверняка помните, что мы пишем wiki для любителей функционального программирования. Каждая страничка посвящена алгебраическому типу.
У нас уже есть готовый запрос
и схема
Пусть кто-нибудь напишет код за нас
Как мы используем GraphQL на клиенте? Обычно нам нужно сделать какой-то запрос, получить данные и показать их в интерфейсе. Давайте напишем компонент, отображающий алгебраический тип данных. Конечно же будем использовать TypeScript:
Читателям предлагается найти косяк в примере выше :)
А косяк-то вот в чем:
В нашем запросы мы забыли поле parametersСount, но радостно используем его
внутри компонента.
Удивительно, что TypeScript позволяет нам это делать. Почему так?
Давайте посмотрим на тип useQuery
export declare function useQuery<TData = any, TVariables = OperationVariables>(
query: DocumentNode | TypedDocumentNode<TData, TVariables>,
options?: QueryHookOptions<TData, TVariables>
): QueryResult<TData, TVariables>
Дефолтное значение типа TData
– any
. Значит, мы можем делать все, что хотим 💥
Исправить проблемку легко. Просто параметризуем useQuery
hook нужным значением
type PageData = {
algebraicDataType: {
name: string
id: string
description: string
}
}
const { data, loading } = useQuery<PageData>(PageQuery)
И так для каждого запроса... Скууучно 🥱 А еще избыточно. У нас уже есть запрос. Линтер уже убедился, что наш запрос валиден, используя схему.
Все нужное уже есть в AlgebraicDataType
типе внутри схемы
Все что нам нужно это автоматически сгенерировать тип ответа. Как это делается?
Давайте установим graphql-codegen
и немного с ним поэкспериментируем, а потом разберемся, как нам использовать этот инструмент для улучшения DX.
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript
Говорим ему init и нам покажут удобный wizard, где можно сгенерировать файл конфига, добавить нужные скрипты в package.json и выбрать нужные нам опции генерации.
yarn graphql-codegen init
Чуть-чуть подредактируем конфиг для наших экспериментов. Он будет выглядеть примерно так:
overwrite: true
schema: 'graphql/schema.graphql'
generates:
generated.tsx:
plugins:
- typescript
Такой сетап сделает очень простую штуку. Он смапит все наши типы на типы из Typescript
type AlgebraicDataType {
id: ID!
name: String!
description: String!
parametersCount: Int!
constructors: [TypeConstructor!]!
isAliasFor: AlgebraicDataType
}
export type AlgebraicDataType = {
__typename?: 'AlgebraicDataType'
constructors: Array<TypeConstructor>
description: Scalars['String']
id: Scalars['ID']
isAliasFor?: Maybe<AlgebraicDataType>
name: Scalars['String']
parametersCount: Scalars['Int']
}
Scalars?
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
}
Это тип, в котором перечислены все скалярные типы в нашей схеме.
Пока не очень-то и полезно. Проблему выше такая кодогенерация решить не поможет. Нужен еще один плагин, который пройдется по всем GraphQL запросам и сгенерирует нам типы ответов.
Собственно вот он – @graphql-codegen/typescript-operations
Устанавливаем его yarn add -D @graphql-codegen/typescript-operations
Чуть-чуть правим конфиги и вжух 🪄
Мы добавили поле documents. Это путь (или пути) к файлам, где codegen будет искать ваши запросы. При этом запрос не обязан лежать отдельно. Если вы укажете путь к .ts или .js файлу, codegen найдет там запрос и сгенерирует для него типы.
overwrite: true
schema: 'graphql/schema.graphql'
documents: './graphql/**/*.graphql'
generates:
generated.tsx:
plugins:
- typescript
- typescript-operations
Получим тип для нашего запроса:
export type GetTypeQuery = {
__typename?: 'Query'
algebraicDataType?: {
__typename?: 'AlgebraicDataType'
name: string
id: string
description: string
} | null
}
Вся мощь кодгена скрыта в его расширяемости. Вы можете добавлять новые плагины или выбирать из существующих. Я опишу несколько, чтобы вы прониклись:
- Если вы используете apollo, вам сгенерируют все хуки для походов на сервер,
- Если вы используете продвинутые фишки apollo, вам сгенерируют типы для ваших
typePolicies
(Если вы не знаете что это такое, ничего страшного :)) - Можно генерировать интроспекцию,
- На сервере можно гененрировать типы для ваших graphql резолверов,
- Плагины для интеграции с React, Vue, Svelte и Ангуляр.
А если то, что мы сгенерировали, не понравится нашим линтерам?
Тогда просто добавляете в конфиг вот такую настройку:
hooks:
afterAllFileWrite: - eslint --fix
Остался финальный вопрос: что делать с файликом, где лежат все наши сгенерированные типы?
Самым простым решением будет положить его в git. Так часто делают, но это принесет вам немного грусти и боли. Попробуйте отребейзить ветку с парочкой новых запросов.
На каждой итерации вы будете ловить merge conflict.
Альтернативное решение – положить файлик в .gitignore. Но тогда, если codegen не был ни разу запущен, сломаются все импорты и проверки на CI.
Вы же будете делать что-то типа import { GetTypeQuery } from './generated.tsx'
.
Я не знаю хорошего плагина для сборщика, который бесшовно сможет интегрировать graphql-codegen в вашу сборку. Если вы знаете такой - делитесь.
А есть что-то еще?
Разумеется! В следующем посте мы поговорим о тестах.