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 — это обещание сервера, нарушение которого всплывает по дереву. Дальше — аргументы и входные типы, которыми клиент управляет выборкой.