Nullability и списки: ! и [Type!]!

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

[User!]! пугает новичка и не значит ничего сложного. Читай справа налево — и любая сигнатура раскладывается на простые слова.

По умолчанию любое поле в GraphQL может вернуть null. Это сознательное решение спецификации: ошибка в одном поле не должна ронять весь ответ, поэтому проблемное поле становится null, а ошибка пишется в errors. Чтобы сказать «это поле обязано быть непустым», к типу добавляют ! — модификатор non-null.

Восклицательный знак

type User {
  id: ID!       # обязан быть, null недопустим
  name: String! # обязан быть
  bio: String   # может быть null (поле необязательно)
}

Если резолвер поля id: ID! вернёт null, это считается ошибкой: GraphQL «поднимет» null вверх по дереву, пока не дойдёт до ближайшего nullable-родителя. Поэтому ! — это обещание, которое сервер обязан держать.

Списки

Квадратные скобки означают «список значений данного типа»:

type Query {
  tags: [String]      # список строк (или null; и элементы могут быть null)
  users: [User!]!     # самый частый и строгий вариант
}

Здесь кроется частый источник путаницы: важно различать «список может быть null» и «элементы списка могут быть null» — это два независимых решения, и каждый ! отвечает за своё. Самое важное — научиться читать [User!]!. Разбираем справа налево:

[ User !  ] !
       |     |
       |     +--- внешний "!": сам список не может быть null
       |          (минимум вернётся пустой массив [])
       +--------- внутренний "!": ни один элемент списка не null

Итог:  [User!]!  =  непустой контейнер списка,
                    внутри только настоящие User (без дыр-null)

Сравни четыре комбинации, чтобы прочувствовать разницу:

СигнатураСам списокЭлементы
[User]может быть nullмогут быть null
[User!]может быть nullне null
[User]!не nullмогут быть null
[User!]!не nullне null

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

При сборке ответа сервер проверяет каждое значение по его типу. Встретил null там, где стоит ! — фиксирует ошибку и «всплывает» null до ближайшего nullable-уровня. Для списков с [Type!] сервер проверяет каждый элемент. Смоделируем эту проверку:

// Проверка значения по сигнатуре [User!]!
function checkList(value) {
  if (value === null || value === undefined)
    return "Ошибка: список не может быть null (внешний !)";
  for (let i = 0; i < value.length; i++) {
    if (value[i] === null)
      return "Ошибка: элемент [" + i + "] не может быть null (внутренний !)";
  }
  return "OK, элементов: " + value.length;
}

console.log(checkList([{id:1}, {id:2}]));  // OK
console.log(checkList([{id:1}, null]));    // ошибка элемента
console.log(checkList(null));              // ошибка списка

Попробуй сам ▶ — подставь пустой массив []. Он валиден для [User!]!: контейнер есть, дыр нет.

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

  • Лепить ! на всё подряд. Non-null — это обещание. Если данные иногда отсутствуют, честнее nullable: иначе одна пустота уронит целую ветку ответа.
  • Путать [User!]! и [User]!. Первое запрещает дыры внутри, второе — только пустой контейнер. Разница критична на клиенте.
  • Сделать nullable-поле обязательным «потом». Превращение String в String! — breaking change для клиентов, которые уже умеют обрабатывать null.

Best practices

  • Списки почти всегда объявляй как [Type!]!: клиенту удобнее работать с гарантированным массивом без дыр (на пустоту — просто []).
  • Идентификаторы (id: ID!) и ключевые отображаемые поля делай non-null — их отсутствие обычно означает баг, а не валидное состояние.
  • Помни про «всплытие null»: одно non-null поле, вернувшее null, может обнулить весь родительский объект. Проектируй обязательность осознанно.

Итоги

По умолчанию поля nullable; ! делает их обязательными, а [] превращает в список. Сложные сигнатуры вроде [User!]! читаются справа налево: внешний знак — про сам список, внутренний — про элементы. Non-null — это обещание сервера, нарушение которого всплывает по дереву. Дальше — аргументы и входные типы, которыми клиент управляет выборкой.

Проверьте себя
1. Что означает сигнатура [User!]! ?
AСписок может быть null, элементы тоже
BСписок не может быть null, и ни один элемент не может быть null
CТолько один User, не список
DСписок из строк
2. Каким будет поле по умолчанию, если не добавить ! ?
ANon-null (обязательным)
BNullable (может вернуть null)
CСписком
DСкаляром