Совмещение Typescript и GraphQL Code Generator

GraphQL  —  это открытый язык запросов и управления данными для API.

Нам больше не нужно играть в догадки, как в случае с REST, поскольку этот язык строго типизирован. Перед выполнением запроса инструменты обеспечивают его синтаксическую верность и соответствие системе типов GraphQL.

Так как TypeScript  —  это типизированный язык, он идеально сочетается с GraphQL. Используя их вместе, можно обеспечить согласование типов в бэкенде и фронтенде.

Зачем нам вручную определять интерфейсы TS? При написании кода вручную велика вероятность появления ошибок, поэтому и был разработан graphql-codegen.

Генератор кода GraphQL  —  это инструмент командной строки, способный генерировать типизацию TypeScript на основе схемы GraphQL. При разработке GraphQL-бэкенда возникает много случаев, когда мы пишем то, что уже описано в схеме, только в ином формате. Например, сигнатуры механизмов интерпретации, модели MongoDB, службы Angular и т.д.

graphql-code-generator.com

Если коротко, то это инструмент командной строки, который выполнит за нас всю трудоемкую работу. В этой статье мы познакомимся с ним поглубже и посмотрим, как его можно использовать для создания запроса с помощью Typescript, React и Apollo.

Настройка генератора кода GraphQL

Начнем с установки библиотеки GraphQL:

yarn add graphqlnpm install — save graphql

Далее нужно добавить cli:

yarn add -D @graphql-codegen/clinpm install --save-dev @graphql-codegen/cli

Генератор кода считывает всю конфигурацию из файла codegen.yml. Можно сгенерировать ее автоматически, воспользовавшись мастером установки. Запустим его:

yarn graphql-codegen initnpx graphql-codegen init

Для нашего примера выберем Typescript и Apollo.

После настройки останется только выполнить типичную команду установки:

npm install / yarn install

Чтобы не усложнять, мы будем потреблять демонстрационную конечную точку GraphQL. Можно найти много таких точек здесь.

После настройки codegen.yml будет выглядеть так:

overwrite: true
schema: https://swapi-graphql.netlify.app/.netlify/functions/index
documents: 'src/**/*.gql'
generates:
  src/generated/graphql.tsx:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-react-apollo'

Параметр schema сообщает инструменту о местонахождении конечной точки GraphQL, в качестве которой мы будем использовать Star Wars.

В этом сценарии мы применяем схему URL, но для ее определения также можно задействовать локальный файл .qql:

schema: 'src/**/*.graphql'

Или указать файл с несколькими шаблонами:

// Несколько шаблонов
schema:
- 'src/app1/**/*.graphql'
- 'src/app2/**/*.graphql'
// ignores files
schema:
- 'src/**/*.graphql'
- '!src/app2/**/*.graphql'

Параметр documents сообщает инструменту командной строки, откуда извлекать gql fragments/mutations/queries.

Имейте в виду, что генератор кода является голым инструментом, настройка которого производится с помощью плагинов. Обратите внимание на раздел plugins в codegen.yml. Так настраивается ядро.

В этой статье мы сосредоточимся на React с Apollo и Typescript, но его также можно использовать и с любыми другими вендорами или средами. Можно даже выводить код в других языках программирования, например в Java, .Net и Kotlin.

Для автоматической генерации всех типов нужно выполнить:

yarn generate

Обратите внимание, что сейчас мы получим сбой, так как еще не создали файл qql для парсинга.

Error: Unable to find any GraphQL type definitions for the following pointers: 'src/**/*.gql'

Создание запроса GraphQL

В этом примере им будет разбитый на страницы запрос для перечисления всех планет из Звездных войн.

Обратиться к конечной точке Star Wars можно здесь.

Вот как там будет выглядеть наш запрос:

{
  allPlanets (first:5, after: "YXJyYXljb25uZWN0aW9uOjQ=") {
    planets {
      id,
      name,
      diameter,
      population,
      gravity
    },
    pageInfo {
      endCursor
    }
  }
}

Создадим на основе этого запроса файл planets.qql:

query allPlanets($after: String) {
  allPlanets(first: 5, after: $after) {
    planets {
      id
      name
      diameter
      population
      gravity
    }
    pageInfo {
      endCursor
    }
  }
}

Обратите внимание, как мы объявляем запрос с приставкой query. Мы используем $ для объявления переменной $after и указываем для нее тип String. Он не является обязательным и не делает ее однозначно String, так как на начальной странице это значение будет пусто.

Теперь можно сгенерировать первый запрос:

npm generate / yarn generate
Успешная генерация

На этот раз вместо сбоя нас ожидал успех, так как теперь есть запрос для генерации в src/queries/planets.gql. Можете проверить сгенерированный файл src/generated/graphql.tsx и начать использовать его в проекте.

Подстройка генератора

Мы уже сгенерировали запрос с базовой конфигурацией. Теперь посмотрим, как можно улучшить генерируемый код более углубленной настройкой.

В этом инструменте командной строки есть набор предустановленных настроек, с которыми можно ознакомиться здесь. Вот некоторые из них:

config:
  withHooks: true
  withComponent: false
  withHOC: false

Поскольку подход Component и HOC устарел, вам будет рекомендована версия hook. Если вы привыкли следовать рекомендациям, то все будет в порядке. 

Есть несколько достойных внимания вариантов конфигурации. Вот три наиболее, на мой взгляд, актуальных:

1. Функциональное программирование

Сгенерированный код будет придерживаться стиля функционального программирования:

config:
  constEnums: true
  immutableTypes: true

Это защитит вас от возможной мутации объектов результата запроса и необходимости использовать стиль кода, не допускающий изменения. В TypeScript очень удобно предотвращать мутации с помощью модификатора readonly.

2. Приставка

В некоторых командах принято добавлять приставки, например, к перечислениям и интерфейсам. В инструменте командной строки для этого есть ряд опций:

config:
  typesPrefix: I
  typesSuffix: I
  enumPrefix: false

3. Стиль кода

Некоторые группы разработчиков не любят необязательные поля или используют их особым образом. Этот инструмент позволяет настроить поведение полей optional:

config:
  avoidOptionals:
  field: true
  inputValue: true
  object: true
  defaultValue: true

В документации по этой части есть и много другой полезной информации.

После настройки конфигурации итоговый файл codegen.yml будет выглядеть так:

overwrite: true
schema: https://swapi-graphql.netlify.app/.netlify/functions/index
documents: 'src/**/*.gql'
generates:
  src/generated/graphql.tsx:
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-react-apollo'
    config:
      constEnums: true
      immutableTypes: true

Настройка клиента Apollo

Для получения возможности использовать и выполнять сгенерированный запрос, нужно настроить Apollo Client.

yarn add @apollo/client graphqlnpm install @apollo/client graphql

@apollo/client: этот пакет содержит практически все необходимое для настройки Apollo Client. Он включает внутренний кэш памяти, управление локальным состоянием, обработку ошибок и уровень представления на основе React.

graphql: этот пакет предоставляет логику для парсинга запросов GraphQL.

apollographql.com

Обратите внимание, что в codegen.yml мы используем typescript-react-apollo, который уже генерирует запросы Apollo.

Далее настроим Apollo Client и Provider:

import '../styles/globals.css'
import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://swapi-graphql.netlify.app/.netlify/functions/index',
  cache: new InMemoryCache()
});

function MyApp({ Component, pageProps }) {
  return (
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  )
}

export default MyApp

Вот теперь можно применять сгенерированный запрос.

Использование сгенерированного GraphQL-запроса

При использовании интеграции Apollo генератор будет предоставлять автоматические или ручные запросы:

// активируется автоматически при рендеринге компонентов
const { data, loading} = useAllPlanetsQuery();
// активируется вручную через вызов метода "getPlanets"
const [getPlanets, { loading, data }] = useAllPlanetsLazyQuery();

В зависимости от ситуации вы будете применять тот или иной вариант  —  тип данных у них будет одинаков. Давайте взглянем на возвращаемый тип для PlanetQuery.

export type AllPlanetsQuery = (
  { readonly __typename?: 'Root' }
  & { readonly allPlanets?: Maybe<(
    { readonly __typename?: 'PlanetsConnection' }
    & { readonly planets?: Maybe<ReadonlyArray<Maybe<(
      { readonly __typename?: 'Planet' }
      & Pick<Planet, 'id' | 'name' | 'diameter' | 'population' | 'gravity'>
    )>>>, readonly pageInfo: (
      { readonly __typename?: 'PageInfo' }
      & Pick<PageInfo, 'endCursor'>
    ) }
  )> }
)

А теперь посмотрим все это в действии:

import '../styles/globals.css'
import { useAllPlanetsQuery } from './generated/graphql';
import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://swapi-graphql.netlify.app/.netlify/functions/index',
  cache: new InMemoryCache()
});

function ListAllPlanets () {
  const { data, loading} = useAllPlanetsQuery();

  return loading ? (
      <div>
        loading...
      </div>
    ) : (
    <ul>
      {data.allPlanets.planets.map((item) => {
        return (
          <li key={item.id}>
            {item.name} - {item.population ? `${item.population} habitants` : 'N/A'} 
          </li>);
      })}
    </ul>
  )
}

function MyApp() {
  return (
    <ApolloProvider client={client}>
      <ListAllPlanets />
    </ApolloProvider>
  )
}

export default MyApp

Так как все генерируется автоматически, можно не бояться потери синхронизации между бэкендом и фронтендом. 

Чтобы извлечь из сгенерированного файла обобщенные типы, выполните следующее: 

import type { Planet } from ‘../generated/graphql’;

А этой инструкцией можно получить тип конкретного запроса и извлечь пользовательские типы:

import type { AllPlanetsQuery } from '../generated/graphql';

Заключение

Мы рассмотрели генерацию простого GraphQL-запроса, но этот инструмент способен и на многое другое. Он предлагает такие возможности, как Fragments, Mutations и многие другие плагины. 

Graphql Code Generator выполняет всю тяжелую работу за нас. Его очень легко настроить и адаптировать под ваши потребности. Этот инструмент уменьшает количество ручной работы, в ходе которой нередко возникают ошибки. 

Удачным решением будет выполнять yarn generate в непрерывной интеграции. Это гарантирует стабильность и целостность в процессе разработки. 

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Jose Granja: Mixing Typescript and GraphQL Code Generator

Предыдущая статьяКак на самом деле работает Git
Следующая статьяВ ожидании Java 16: Stream.toList() и другие методы преобразования