payload

Use when working with Payload projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.

Install
npx skills add 'https://github.com/payloadcms/payload/tree/main/packages/payload/skills/payload'
Download bundle ↓
main · 9a2cdcbScanned 2026-09-17

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗
View on GitHub
← Back to SKILL.md

Payload Field Type Guards Reference

Complete reference with detailed examples and patterns. See FIELDS.md for quick reference table of all guards.

Structural Guards

fieldHasSubFields

Checks if field contains nested fields (group, array, row, or collapsible).

import type { Field } from 'payload'
import { fieldHasSubFields } from 'payload'

function traverseFields(fields: Field[]): void {
  fields.forEach((field) => {
    if (fieldHasSubFields(field)) {
      // Safe to access field.fields
      traverseFields(field.fields)
    }
  })
}

Signature:

fieldHasSubFields<TField extends ClientField | Field>(
  field: TField
): field is TField & (FieldWithSubFieldsClient | FieldWithSubFields)

Common Pattern - Exclude Arrays:

if (fieldHasSubFields(field) && !fieldIsArrayType(field)) {
  // Groups, rows, collapsibles only (not arrays)
}

fieldIsArrayType

Checks if field type is 'array'.

import { fieldIsArrayType } from 'payload'

if (fieldIsArrayType(field)) {
  // field.type === 'array'
  console.log(`Min rows: ${field.minRows}`)
  console.log(`Max rows: ${field.maxRows}`)
}

Signature:

fieldIsArrayType<TField extends ClientField | Field>(
  field: TField
): field is TField & (ArrayFieldClient | ArrayField)

fieldIsBlockType

Checks if field type is 'blocks'.

import { fieldIsBlockType } from 'payload'

if (fieldIsBlockType(field)) {
  // field.type === 'blocks'
  field.blocks.forEach((block) => {
    console.log(`Block: ${block.slug}`)
  })
}

Signature:

fieldIsBlockType<TField extends ClientField | Field>(
  field: TField
): field is TField & (BlocksFieldClient | BlocksField)

Common Pattern - Distinguish Containers:

if (fieldIsArrayType(field)) {
  // Handle array rows
} else if (fieldIsBlockType(field)) {
  // Handle block types
}

fieldIsGroupType

Checks if field type is 'group'.

import { fieldIsGroupType } from 'payload'

if (fieldIsGroupType(field)) {
  // field.type === 'group'
  console.log(`Interface: ${field.interfaceName}`)
}

Signature:

fieldIsGroupType<TField extends ClientField | Field>(
  field: TField
): field is TField & (GroupFieldClient | GroupField)

Capability Guards

fieldSupportsMany

Checks if field can have multiple values (select, relationship, or upload with hasMany).

import { fieldSupportsMany } from 'payload'

if (fieldSupportsMany(field)) {
  // field.type is 'select' | 'relationship' | 'upload'
  // Safe to check field.hasMany
  if (field.hasMany) {
    console.log('Field accepts multiple values')
  }
}

Signature:

fieldSupportsMany<TField extends ClientField | Field>(
  field: TField
): field is TField & (FieldWithManyClient | FieldWithMany)

fieldHasMaxDepth

Checks if field is relationship/upload/join with numeric maxDepth property.

import { fieldHasMaxDepth } from 'payload'

if (fieldHasMaxDepth(field)) {
  // field.type is 'upload' | 'relationship' | 'join'
  // AND field.maxDepth is number
  const remainingDepth = field.maxDepth - currentDepth
}

Signature:

fieldHasMaxDepth<TField extends ClientField | Field>(
  field: TField
): field is TField & (FieldWithMaxDepthClient | FieldWithMaxDepth)

fieldShouldBeLocalized

Checks if field needs localization handling (accounts for parent localization).

import { fieldShouldBeLocalized } from 'payload'

function processField(field: Field, parentIsLocalized: boolean) {
  if (fieldShouldBeLocalized({ field, parentIsLocalized })) {
    // Create locale-specific table or index
  }
}

Signature:

fieldShouldBeLocalized({
  field,
  parentIsLocalized,
}: {
  field: ClientField | ClientTab | Field | Tab
  parentIsLocalized: boolean
}): boolean
// Accounts for parent localization
if (fieldShouldBeLocalized({ field, parentIsLocalized: false })) {
  /* ... */
}

fieldIsVirtual

Checks if field is virtual (computed or virtual relationship).

import { fieldIsVirtual } from 'payload'

if (fieldIsVirtual(field)) {
  // field.virtual is truthy
  if (typeof field.virtual === 'string') {
    // Virtual relationship path
    console.log(`Virtual path: ${field.virtual}`)
  } else {
    // Computed virtual field (uses hooks)
  }
}

Signature:

fieldIsVirtual(field: Field | Tab): boolean

Data Guards

fieldAffectsData

Most commonly used guard. Checks if field stores data (has name and is not UI-only).

import { fieldAffectsData } from 'payload'

function generateSchema(fields: Field[]) {
  fields.forEach((field) => {
    if (fieldAffectsData(field)) {
      // Safe to access field.name
      schema[field.name] = getFieldType(field)
    }
  })
}

Signature:

fieldAffectsData<TField extends ClientField | Field | TabAsField | TabAsFieldClient>(
  field: TField
): field is TField & (FieldAffectingDataClient | FieldAffectingData)

Pattern - Data Fields Only:

const dataFields = fields.filter(fieldAffectsData)

fieldIsPresentationalOnly

Checks if field is UI-only (type 'ui').

import { fieldIsPresentationalOnly } from 'payload'

if (fieldIsPresentationalOnly(field)) {
  // field.type === 'ui'
  // Skip in data operations, GraphQL schema, etc.
  return
}

Signature:

fieldIsPresentationalOnly<TField extends ClientField | Field | TabAsField | TabAsFieldClient>(
  field: TField
): field is TField & (UIFieldClient | UIField)

fieldIsID

Checks if field name is exactly 'id'.

import { fieldIsID } from 'payload'

if (fieldIsID(field)) {
  // field.name === 'id'
  // Special handling for ID field
}

Signature:

fieldIsID<TField extends ClientField | Field>(
  field: TField
): field is { name: 'id' } & TField

fieldIsHiddenOrDisabled

Checks if field is hidden or admin-disabled.

import { fieldIsHiddenOrDisabled } from 'payload'

const visibleFields = fields.filter((field) => !fieldIsHiddenOrDisabled(field))

Signature:

fieldIsHiddenOrDisabled<TField extends ClientField | Field | TabAsField | TabAsFieldClient>(
  field: TField
): field is { admin: { hidden: true } } & TField

Layout Guards

fieldIsSidebar

Checks if field is positioned in sidebar.

import { fieldIsSidebar } from 'payload'

const [mainFields, sidebarFields] = fields.reduce(
  ([main, sidebar], field) => {
    if (fieldIsSidebar(field)) {
      return [main, [...sidebar, field]]
    }
    return [[...main, field], sidebar]
  },
  [[], []],
)

Signature:

fieldIsSidebar<TField extends ClientField | Field | TabAsField | TabAsFieldClient>(
  field: TField
): field is { admin: { position: 'sidebar' } } & TField

Tab & Group Guards

tabHasName

Checks if tab is named (stores data under tab name).

import { tabHasName } from 'payload'

tabs.forEach((tab) => {
  if (tabHasName(tab)) {
    // tab.name exists
    dataPath.push(tab.name)
  }
  // Process tab.fields
})

Signature:

tabHasName<TField extends ClientTab | Tab>(
  tab: TField
): tab is NamedTab & TField

groupHasName

Checks if group is named (stores data under group name).

import { groupHasName } from 'payload'

if (groupHasName(group)) {
  // group.name exists
  return data[group.name]
}

Signature:

groupHasName(group: Partial<NamedGroupFieldClient>): group is NamedGroupFieldClient

Option & Value Guards

optionIsObject

Checks if option is object format {label, value} vs string.

import { optionIsObject } from 'payload'

field.options.forEach((option) => {
  if (optionIsObject(option)) {
    console.log(`${option.label}: ${option.value}`)
  } else {
    console.log(option) // string value
  }
})

Signature:

optionIsObject(option: Option): option is OptionObject

optionsAreObjects

Checks if entire options array contains objects.

import { optionsAreObjects } from 'payload'

if (optionsAreObjects(field.options)) {
  // All options are OptionObject[]
  const labels = field.options.map((opt) => opt.label)
}

Signature:

optionsAreObjects(options: Option[]): options is OptionObject[]

optionIsValue

Checks if option is string value (not object).

import { optionIsValue } from 'payload'

if (optionIsValue(option)) {
  // option is string
  const value = option
}

Signature:

optionIsValue(option: Option): option is string

valueIsValueWithRelation

Checks if relationship value is polymorphic format {relationTo, value}.

import { valueIsValueWithRelation } from 'payload'

if (valueIsValueWithRelation(fieldValue)) {
  // fieldValue.relationTo exists
  // fieldValue.value exists
  console.log(`Related to ${fieldValue.relationTo}: ${fieldValue.value}`)
}

Signature:

valueIsValueWithRelation(value: unknown): value is ValueWithRelation

Common Patterns

Recursive Field Traversal

import { fieldAffectsData, fieldHasSubFields } from 'payload'

function traverseFields(fields: Field[], callback: (field: Field) => void) {
  fields.forEach((field) => {
    if (fieldAffectsData(field)) {
      callback(field)
    }

    if (fieldHasSubFields(field)) {
      traverseFields(field.fields, callback)
    }
  })
}

Filter Data-Bearing Fields

import { fieldAffectsData, fieldIsPresentationalOnly, fieldIsHiddenOrDisabled } from 'payload'

const dataFields = fields.filter(
  (field) =>
    fieldAffectsData(field) && !fieldIsPresentationalOnly(field) && !fieldIsHiddenOrDisabled(field),
)

Container Type Switching

import { fieldIsArrayType, fieldIsBlockType, fieldHasSubFields } from 'payload'

if (fieldIsArrayType(field)) {
  // Handle array-specific logic
} else if (fieldIsBlockType(field)) {
  // Handle blocks-specific logic
} else if (fieldHasSubFields(field)) {
  // Handle group/row/collapsible
}

Safe Property Access

import { fieldSupportsMany, fieldHasMaxDepth } from 'payload'

// Without guard - TypeScript error
// if (field.hasMany) { /* ... */ }

// With guard - safe access
if (fieldSupportsMany(field) && field.hasMany) {
  console.log('Multiple values supported')
}

if (fieldHasMaxDepth(field)) {
  const depth = field.maxDepth // TypeScript knows this is number
}

Type Preservation

All guards preserve the original type constraint:

import type { ClientField, Field } from 'payload'
import { fieldHasSubFields } from 'payload'

function processServerField(field: Field) {
  if (fieldHasSubFields(field)) {
    // field is Field & FieldWithSubFields (not ClientField)
  }
}

function processClientField(field: ClientField) {
  if (fieldHasSubFields(field)) {
    // field is ClientField & FieldWithSubFieldsClient
  }
}
Referenced from SKILL.md