predicates

star 2

Predicate functions and type guards for clean, self-documenting code. Use when writing boolean conditions, type narrowing, validation logic, or creating reusable predicate functions. Covers isEmpty, hasValue, isValid, can/has/should naming patterns.

eli-eric By eli-eric schedule Updated 2/6/2026

name: predicates description: Predicate functions and type guards for clean, self-documenting code. Use when writing boolean conditions, type narrowing, validation logic, or creating reusable predicate functions. Covers isEmpty, hasValue, isValid, can/has/should naming patterns. user-invocable: false

Predicates & Helper Functions Guide

The application uses centralized predicates and helper functions to promote code reusability and self-documenting logic.

Organization Pattern

Predicates are organized by domain in /src/lib/predicates/:

/src/lib/predicates/
├── data.ts           # Common data predicates (isEmpty, hasValue, etc.)
├── validation.ts     # Validation predicates (isValidEmail, isValidUUID, etc.)
├── type-guards.ts    # Type guard predicates (isDefined, isString, etc.)
└── domain.ts         # Domain-specific predicates (isSystemActive, hasPermission, etc.)

Common Data Predicates (data.ts)

Use these predicates for common data checks:

// Null/undefined/empty checks
export const isEmpty = (value: unknown): boolean =>
    value === null || value === undefined || value === ''

export const isNotEmpty = (value: unknown): boolean => !isEmpty(value)

export const hasValue = <T>(value: T | null | undefined): value is T =>
    value !== null && value !== undefined

export const isNullOrUndefined = (value: unknown): value is null | undefined =>
    value === null || value === undefined

// Array checks
export const isEmptyArray = (arr: unknown[]): boolean => arr.length === 0

export const hasItems = <T>(arr: T[]): boolean => arr.length > 0

// Object checks
export const isEmptyObject = (obj: Record<string, unknown>): boolean =>
    Object.keys(obj).length === 0

export const hasProperties = (obj: Record<string, unknown>): boolean => Object.keys(obj).length > 0

Usage:

import { hasValue, hasItems } from '@/lib/predicates/data'

// Good - readable and self-documenting
if (hasValue(user.email) && hasItems(user.roles)) {
    // Process user
}

// Bad - inline logic
if (user.email !== null && user.email !== undefined && user.roles.length > 0) {
    // Process user
}

Validation Predicates (validation.ts)

Use these predicates for input validation:

// Email validation
export const isValidEmail = (email: string): boolean => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)

// URL validation
export const isValidUrl = (url: string): boolean => {
    try {
        new URL(url)
        return true
    } catch {
        return false
    }
}

// UUID validation
export const isValidUUID = (uuid: string): boolean =>
    /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid)

// Phone number validation
export const isValidPhoneNumber = (phone: string): boolean => /^\+?[\d\s-()]+$/.test(phone)

// Password strength
export const isStrongPassword = (password: string): boolean =>
    password.length >= 8 &&
    /[a-z]/.test(password) &&
    /[A-Z]/.test(password) &&
    /[0-9]/.test(password)

Usage:

import { isValidEmail, isValidUUID } from '@/lib/predicates/validation'

// Form validation
const validateForm = (data: FormData) => {
    if (!isValidEmail(data.email)) {
        return { email: 'Invalid email address' }
    }
    if (!isValidUUID(data.userId)) {
        return { userId: 'Invalid user ID format' }
    }
    return null
}

Type Guard Predicates (type-guards.ts)

Use these predicates for TypeScript type narrowing:

// Basic type guards
export const isDefined = <T>(value: T | null | undefined): value is T =>
    value !== null && value !== undefined

export const isString = (value: unknown): value is string => typeof value === 'string'

export const isNumber = (value: unknown): value is number =>
    typeof value === 'number' && !isNaN(value)

export const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean'

export const isArray = <T>(value: unknown): value is T[] => Array.isArray(value)

export const isObject = (value: unknown): value is Record<string, unknown> =>
    typeof value === 'object' && value !== null && !Array.isArray(value)

// Advanced type guards
export const isNonEmptyString = (value: unknown): value is string =>
    isString(value) && value.length > 0

export const isNonEmptyArray = <T>(value: unknown): value is T[] =>
    isArray(value) && value.length > 0

Usage:

import { isDefined, isNonEmptyString } from '@/lib/predicates/type-guards'

// TypeScript type narrowing
function processData(data: string | null | undefined) {
    if (isDefined(data)) {
        // TypeScript knows data is string here
        return data.toUpperCase()
    }
    return null
}

// Filter with type guards
const validStrings = items.filter(isNonEmptyString)
// validStrings is typed as string[]

Domain-Specific Predicates (domain.ts)

Use these predicates for application-specific logic:

import type { User, System, Resource } from '@/types'

// System predicates
export const isSystemActive = (system: { status: string }): boolean => system.status === 'active'

export const isSystemInMaintenance = (system: { status: string }): boolean =>
    system.status === 'maintenance'

// Permission predicates
export const hasEditPermission = (user: User, resource: Resource): boolean =>
    user.role === 'admin' || resource.ownerId === user.id

export const canEdit = (permissions: string[]): boolean =>
    permissions.includes('edit') || permissions.includes('admin')

export const canDelete = (permissions: string[]): boolean =>
    permissions.includes('delete') || permissions.includes('admin')

export const isAdmin = (user: User): boolean => user.role === 'admin'

// Status predicates
export const isPending = (status: string): boolean => status === 'pending'
export const isCompleted = (status: string): boolean => status === 'completed'
export const isCancelled = (status: string): boolean => status === 'cancelled'

Usage:

import { isSystemActive, hasEditPermission } from '@/lib/predicates/domain'

// UI conditional rendering
{isSystemActive(system) && (
  <Button onClick={handleEdit}>Edit System</Button>
)}

// Access control
if (hasEditPermission(currentUser, resource)) {
  // Allow edit
}

Best Practices

1. Extract Complex Conditions

// Good - extract to predicate
const canSubmitForm = (form: FormData): boolean =>
    hasValue(form.email) &&
    isValidEmail(form.email) &&
    hasValue(form.password) &&
    isStrongPassword(form.password)

if (canSubmitForm(formData)) {
    // Submit
}

// Bad - inline complexity
if (
    formData.email !== null &&
    formData.email !== undefined &&
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) &&
    formData.password !== null &&
    formData.password !== undefined &&
    formData.password.length >= 8
) {
    // Submit
}

2. Use Type Guards for Type Safety

// Good - provides type narrowing
import { isDefined, isNonEmptyArray } from '@/lib/predicates/type-guards'

function processItems(items: Item[] | null | undefined) {
    if (isDefined(items) && isNonEmptyArray(items)) {
        // TypeScript knows items is Item[] here
        return items.map(item => item.name)
    }
    return []
}

3. Name Predicates Clearly

Use prefixes that indicate boolean return:

  • is - state checks (isEmpty, isActive, isValid)
  • has - possession checks (hasValue, hasPermission, hasItems)
  • can - capability checks (canEdit, canDelete, canSubmit)
  • should - conditional logic (shouldShow, shouldEnable, shouldValidate)

Examples:

// State checks
const isActive = (status: string): boolean => status === 'active'
const isEmpty = (value: string): boolean => value === ''
const isValid = (data: FormData): boolean => validateData(data)

// Possession checks
const hasValue = (val: unknown): boolean => val !== null && val !== undefined
const hasPermission = (user: User, perm: string): boolean => user.permissions.includes(perm)
const hasItems = (arr: unknown[]): boolean => arr.length > 0

// Capability checks
const canEdit = (user: User): boolean => user.role === 'editor' || user.role === 'admin'
const canDelete = (user: User, resource: Resource): boolean => user.id === resource.ownerId
const canSubmit = (form: FormData): boolean => form.isValid && !form.isSubmitting

// Conditional logic
const shouldShow = (condition: boolean): boolean => condition && isUserLoggedIn()
const shouldEnable = (feature: string): boolean => features.includes(feature)
const shouldValidate = (field: string): boolean => field.length > 0

4. Keep Predicates Pure

// Good - pure function, no side effects
export const isValidUser = (user: User): boolean => hasValue(user.email) && isValidEmail(user.email)

// Bad - has side effects
export const isValidUser = (user: User): boolean => {
    console.log('Validating user') // Side effect!
    logToAnalytics('validation') // Side effect!
    return hasValue(user.email) && isValidEmail(user.email)
}

5. Organize by Domain

Place predicates in the appropriate file:

  • General purpose -> data.ts or type-guards.ts
  • Input validation -> validation.ts
  • Business logic -> domain.ts

Real-World Examples

Form Validation

import { hasValue, isValidEmail, isStrongPassword } from '@/lib/predicates'

const validateRegistrationForm = (data: RegistrationFormData) => {
    const errors: Record<string, string> = {}

    if (!hasValue(data.email)) {
        errors.email = 'Email is required'
    } else if (!isValidEmail(data.email)) {
        errors.email = 'Invalid email format'
    }

    if (!hasValue(data.password)) {
        errors.password = 'Password is required'
    } else if (!isStrongPassword(data.password)) {
        errors.password =
            'Password must be at least 8 characters with uppercase, lowercase, and numbers'
    }

    return Object.keys(errors).length > 0 ? errors : null
}

Conditional Rendering

import { isSystemActive, hasEditPermission } from '@/lib/predicates/domain'
import { hasValue } from '@/lib/predicates/data'

const SystemCard = ({ system, currentUser }) => {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{system.name}</CardTitle>
        {isSystemActive(system) && (
          <Badge variant="success">Active</Badge>
        )}
      </CardHeader>
      <CardContent>
        {hasValue(system.description) && (
          <p>{system.description}</p>
        )}
      </CardContent>
      <CardFooter>
        {hasEditPermission(currentUser, system) && (
          <Button onClick={handleEdit}>Edit</Button>
        )}
      </CardFooter>
    </Card>
  )
}

Data Filtering

import { isDefined, isNonEmptyArray } from '@/lib/predicates/type-guards'
import { isSystemActive } from '@/lib/predicates/domain'

const getActiveSystems = (systems: System[] | null | undefined) => {
    if (!isDefined(systems) || !isNonEmptyArray(systems)) {
        return []
    }

    return systems.filter(isSystemActive)
}

When to Use Predicates vs Helpers

Use Predicates when:

  • Function returns a boolean
  • Checking state, conditions, or validation
  • Type narrowing with TypeScript
  • Self-documenting conditional logic

Use Helpers when:

  • Transforming data (formatting, mapping, reducing)
  • Performing calculations
  • Utility functions that return non-boolean values
  • Side effects (fetching data, logging, etc.)
// Predicate - returns boolean
const isValidEmail = (email: string): boolean => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)

// Helper - transforms data
const formatEmail = (email: string): string => email.toLowerCase().trim()

// Predicate - checks condition
const hasDiscount = (user: User): boolean => user.membershipLevel === 'premium'

// Helper - calculates value
const calculateDiscount = (price: number, user: User): number =>
    hasDiscount(user) ? price * 0.9 : price

Testing Predicates

Predicates are easy to test because they're pure functions:

import { isValidEmail, hasValue } from '@/lib/predicates'

describe('isValidEmail', () => {
    it('returns true for valid emails', () => {
        expect(isValidEmail('test@example.com')).toBe(true)
    })

    it('returns false for invalid emails', () => {
        expect(isValidEmail('invalid')).toBe(false)
        expect(isValidEmail('test@')).toBe(false)
    })
})

describe('hasValue', () => {
    it('returns true for defined values', () => {
        expect(hasValue('test')).toBe(true)
        expect(hasValue(0)).toBe(true)
    })

    it('returns false for null/undefined', () => {
        expect(hasValue(null)).toBe(false)
        expect(hasValue(undefined)).toBe(false)
    })
})
Install via CLI
npx skills add https://github.com/eli-eric/ELI-panda --skill predicates
Repository Details
star Stars 2
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator