Files
slm-design/.opencode/skills/style-guide/reference/value-predicates/README.md
2026-08-01 09:31:08 +03:00

4.8 KiB
Raw Blame History

Value Predicates

value-predicates — небольшая библиотека runtime-предикатов для безопасной работы с unknown, null, массивами и объектами.

Файл содержит два типа утилит:

  • Type guards: возвращают value is T и сужают тип в TypeScript.
  • Boolean-предикаты: возвращают boolean и используются для читаемых условий без обязательного narrowing.

Проверки доменных DTO и API-ответов не размещаются здесь. Они должны жить рядом с владельцем данных: business-модулем, mapper/source или infra-адаптером.

Группы

  • Nullish: isDefined, isNotDefined.
  • Primitives: isString, isNumber, isBoolean.
  • Strings: isNonEmptyString.
  • Arrays: isArray, isArrayOf, isEmptyArray, isNonEmptyArray.
  • Objects: isRecord, hasOwn.
  • Combinators: isOneOf.

Правило для TSX

В conditional rendering не пишем голые проверки длины массива:

items?.length
items.length > 0
items.length !== 0
items.length === 0
!items.length
items && items.map(...)

Для списков используем isEmptyArray, isNonEmptyArray, а для unknown-данных — isArrayOf.

Голый .length допустим, когда нужен именно числовой размер для текста, расчётов или атрибутов.

Уже типизированный массив

const ordersData = orders.data

{isNonEmptyArray(ordersData) && (
  <OrdersList orders={ordersData} />
)}

В этом сценарии ordersData уже имеет тип вроде Order[] | null | undefined, поэтому достаточно проверить, что массив существует и не пуст.

Empty state

const ordersData = orders.data
const shouldShowEmptyState = isEmptyArray(ordersData) && !orders.isLoading

{shouldShowEmptyState && (
  <EmptyState />
)}

isEmptyArray считает null и undefined пустым списком. Это удобно для UI-состояний, где отсутствие данных и пустой список показывают один empty state.

Map в JSX

const itemsData = items

{isNonEmptyArray(itemsData) && itemsData.map((item) => (
  <Card key={item.id} item={item} />
))}

Сначала сужаем локальную переменную, потом используем её в map. Не дублируем путь к данным внутри JSX.

Unknown/API данные

type Order = {
  id: string
  title: string
}

const isOrder = (value: unknown): value is Order => {
  return (
    isRecord(value) &&
    hasOwn(value, 'id') &&
    isString(value.id) &&
    hasOwn(value, 'title') &&
    isString(value.title)
  )
}

const ordersData = response.data

{isArrayOf(ordersData, isOrder) && (
  <OrdersList orders={ordersData} />
)}

isArrayOf нужен на границе с недоверенными данными: он проверяет не только массив, но и каждый элемент через item guard.

Если нужно одновременно проверить форму элементов и непустой массив:

const canRenderOrders = isArrayOf(ordersData, isOrder) && isNonEmptyArray(ordersData)

{canRenderOrders && (
  <OrdersList orders={ordersData} />
)}

Если такой паттерн повторится много раз, можно добавить отдельный isNonEmptyArrayOf, но заранее его не вводим.

Object fields

if (isRecord(value) && hasOwn(value, 'code') && isString(value.code)) {
  // value.code: string
}

isRecord проверяет только базовую форму объекта: не null и не массив. Конкретные поля всегда проверяются отдельно.

Literal unions

const statuses = ['draft', 'published'] as const

if (isOneOf(value, statuses)) {
  // value: 'draft' | 'published'
}

isOneOf удобен для runtime-проверки union-типов, собранных из as const массивов.

Нормализованный массив

Если массив заранее нормализован, прямой map допустим:

const ordersData = orders.data ?? []

return ordersData.map((order) => (
  <OrderCard key={order.id} order={order} />
))

Для conditional rendering empty state всё равно используем predicate:

{isEmptyArray(ordersData) && (
  <EmptyState />
)}