Скаляры, объекты и перечисления

Из чего собрана любая схема: скаляры (примитивы вроде Int и String), объектные типы (составные сущности) и перечисления (фиксированные наборы значений).

Скаляры — это листья дерева, объекты — его ветви. Запрос всегда обязан дойти до листьев-скаляров, иначе он недописан.

Система типов GraphQL стоит на нескольких видах именованных типов. Всего их шесть: объектные, скалярные, перечисления (enum), интерфейсы, объединения (union) и входные (input) типы. Сейчас разберём три самых базовых, с которыми ты будешь работать постоянно — на них держится 90% любой схемы, а остальные три встретятся позже в курсе по мере надобности.

Скалярные типы

Скаляры — это конкретные значения, дальше них «нырять» некуда. В спецификацию встроены пять:

СкалярЧто хранит
IntЦелое 32-битное число
FloatЧисло с плавающей точкой
StringСтрока UTF-8
Booleantrue или false
IDУникальный идентификатор (сериализуется как строка)

Особый случай — ID. Внешне он как строка, но семантически означает «уникальный ключ» и не предназначен для показа человеку. Можно объявлять и кастомные скаляры — например DateTime, Email, JSON — со своей логикой проверки и сериализации:

scalar DateTime

type Event {
  id: ID!
  title: String!
  startsAt: DateTime!
}

Объектные типы

Объектный тип — это составная сущность с набором полей. Поля могут быть скалярами или ссылками на другие объектные типы — так и строится граф:

type Author {
  id: ID!
  name: String!
}

type Book {
  id: ID!
  title: String!
  author: Author!     # ссылка на другой объектный тип
}

Перечисления (enum)

Enum ограничивает поле фиксированным набором именованных значений — отличная защита от «магических строк»:

enum Role {
  ADMIN
  EDITOR
  VIEWER
}

type Member {
  id: ID!
  role: Role!
}

Клиент не сможет передать role: "boss" — валидатор отклонит всё, что не входит в перечисление.

Как работает под капотом

Для каждого скаляра у сервера есть функции сериализации (значение из БД -> в ответ) и парсинга (значение из запроса -> во внутренний вид). Для кастомного скаляра ты пишешь эти функции сам. Объектные поля, ссылающиеся на другие типы, рекурсивно резолвятся вглубь, пока обход не упрётся в скаляры-листья. Enum проверяется как принадлежность значения списку.

Смоделируем валидацию enum и приведение скаляра в JS:

const Role = ["ADMIN", "EDITOR", "VIEWER"];

function setRole(value) {
  if (!Role.includes(value)) {
    return "Ошибка enum: '" + value + "' не из " + Role.join("|");
  }
  return "ok: " + value;
}

// кастомный скаляр DateTime: парсим строку в timestamp
function parseDateTime(s) {
  const t = Date.parse(s);
  return isNaN(t) ? "невалидная дата" : t;
}

console.log(setRole("ADMIN"));            // ok
console.log(setRole("boss"));             // ошибка enum
console.log(parseDateTime("2024-05-01")); // число-таймстамп

Попробуй сам ▶ — передай в setRole значение не из списка и в parseDateTime «не дату». Так сервер защищает данные от мусора.

Частые ошибки

  • Хранить идентификаторы как Int. Лучше ID: он не привязан к числовому формату и переживёт смену схемы БД.
  • Запросить объектное поле без вложенных полей. author без { name } внутри — ошибка: до скаляров надо дойти.
  • Свободные строки вместо enum. Если у поля конечный набор значений (статусы, роли), enum избавит от опечаток и задокументирует варианты.

Best practices

  • Заводи кастомные скаляры для дат, email, URL — это переносит проверку формата на уровень схемы.
  • Используй enum для всего, что имеет фиксированный набор значений; добавлять новые значения в enum — безопасное изменение.
  • Имена enum-значений пиши в UPPER_SNAKE_CASE — это устоявшееся соглашение.

Итоги

Типы GraphQL собираются из скаляров (Int, Float, String, Boolean, ID и кастомных), объектных типов (составные сущности со ссылками) и перечислений (фиксированные наборы значений). Запрос всегда должен дойти до скаляров-листьев. Дальше посмотрим на модификаторы ! и [], которые задают обязательность и списки.

Проверьте себя
1. Чем поле типа ID отличается от обычной строки?
AЭто всегда число
BЭто уникальный идентификатор, не предназначенный для показа человеку, сериализуется как строка
CОно не может быть обязательным
DОно хранит дату
2. Зачем в схеме нужен enum?
AЧтобы хранить большие тексты
BЧтобы ограничить поле фиксированным набором именованных значений и избежать опечаток
CЧтобы ускорить запросы
DЧтобы заменить объектные типы