8/7/2022

Магия декларативности и схемы. Часть 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>

Дефолтное значение типа TDataany. Значит, мы можем делать все, что хотим 💥

Исправить проблемку легко. Просто параметризуем 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 в вашу сборку. Если вы знаете такой - делитесь.

А есть что-то еще?

Разумеется! В следующем посте мы поговорим о тестах.