← Курс/Props: валидация и значения по умолчанию#224 из 257+25 XP

Props: валидация и значения по умолчанию

Базовое объявление props с типами

В Vue 3 с Composition API props объявляются через defineProps(). Самый простой вариант — массив строк, но для продакшена нужна полноценная валидация:

// Полное объявление с валидацией
const props = defineProps({
  title: {
    type: String,
    required: true,
  },
  count: {
    type: Number,
    default: 0,
  },
  status: {
    type: String,
    default: 'active',
    validator: (value) => ['active', 'inactive', 'pending'].includes(value),
  },
})

Vue выводит предупреждения в консоль при нарушении валидации — только в режиме разработки.

withDefaults() и TypeScript

При использовании TypeScript-синтаксиса для defineProps дефолтные значения задаются через withDefaults():

// TypeScript-стиль
const props = withDefaults(defineProps<{
  title: string
  count?: number
  tags?: string[]
}>(), {
  count: 0,
  tags: () => [],  // для массивов/объектов — фабричная функция!
})

Обратите внимание: для массивов и объектов дефолтное значение должно быть **функцией** (как в data() Options API), иначе все экземпляры компонента будут делить одну ссылку.

Boolean props

Props типа Boolean имеют особое поведение: их можно передавать без значения:

<!-- Эти записи эквивалентны: -->
<MyButton disabled />
<MyButton :disabled="true" />

<!-- А это — тоже эквивалентно: -->
<MyButton />
<!-- :disabled="false" — не передан -->
const props = defineProps({
  disabled: {
    type: Boolean,
    default: false,
  },
  loading: Boolean,  // Краткая запись, default = false
})

Validator: кастомная валидация

Функция validator получает значение и должна вернуть true/false:

const props = defineProps({
  age: {
    type: Number,
    validator: (val) => val >= 0 && val <= 150,
  },
  color: {
    type: String,
    validator: (val) => /^#[0-9A-Fa-f]{6}$/.test(val),
  },
  size: {
    type: String,
    default: 'md',
    validator: (val) => ['xs', 'sm', 'md', 'lg', 'xl'].includes(val),
  },
})

required vs optional

  • required: true — Vue выдаст предупреждение, если prop не передан
  • Props без required и без default — по умолчанию необязательны, значение будет undefined
  • Нельзя одновременно указывать required: true и default — это противоречие
  • const props = defineProps({
      id: { type: Number, required: true },    // обязательный
      label: { type: String, default: '' },    // необязательный с дефолтом
      extra: String,                           // необязательный, может быть undefined
    })

    Несколько допустимых типов

    const props = defineProps({
      value: [String, Number],        // строка или число
      callback: [Function, null],     // функция или null
    })

    Примеры

    Система валидации props — аналог того, как Vue проверяет значения во время выполнения

    // Реализуем упрощённую систему валидации props,
    // аналогичную тому, что делает Vue внутри.
    
    function createPropsValidator(schema) {
      return function validateProps(props) {
        const warnings = []
    
        for (const [key, def] of Object.entries(schema)) {
          const value = props[key]
          const isDefined = value !== undefined && value !== null
    
          // Проверка required
          if (def.required && !isDefined) {
            warnings.push(`[Props] Prop "${key}" обязателен, но не передан`)
            continue
          }
    
          // Применяем default если не передан
          if (!isDefined && def.default !== undefined) {
            props[key] = typeof def.default === 'function'
              ? def.default()
              : def.default
            continue
          }
    
          if (!isDefined) continue
    
          // Проверка типа
          if (def.type) {
            const types = Array.isArray(def.type) ? def.type : [def.type]
            const typeNames = types.map(t => t.name)
            const valid = types.some(t => {
              if (t === Boolean) return typeof value === 'boolean'
              if (t === String) return typeof value === 'string'
              if (t === Number) return typeof value === 'number'
              if (t === Array) return Array.isArray(value)
              if (t === Object) return typeof value === 'object' && !Array.isArray(value)
              return value instanceof t
            })
            if (!valid) {
              warnings.push(`[Props] Prop "${key}": ожидался тип ${typeNames.join('|')}, получено ${typeof value}`)
            }
          }
    
          // Проверка validator
          if (def.validator && isDefined) {
            if (!def.validator(value)) {
              warnings.push(`[Props] Prop "${key}": значение "${value}" не прошло валидацию`)
            }
          }
        }
    
        return { props, warnings }
      }
    }
    
    // Описание схемы (как в defineProps)
    const validate = createPropsValidator({
      title:  { type: String, required: true },
      count:  { type: Number, default: 0 },
      status: {
        type: String,
        default: 'active',
        validator: v => ['active', 'inactive', 'pending'].includes(v),
      },
      tags: { type: Array, default: () => [] },
      size: {
        type: String,
        default: 'md',
        validator: v => ['sm', 'md', 'lg'].includes(v),
      },
    })
    
    function renderComponent(rawProps) {
      const { props, warnings } = validate({ ...rawProps })
      warnings.forEach(w => console.warn(w))
      console.log('Итоговые props:', props)
      console.log('---')
    }
    
    console.log('=== Корректные props ===')
    renderComponent({ title: 'Привет', count: 5, status: 'active' })
    
    console.log('=== Отсутствует required ===')
    renderComponent({ count: 3 })
    
    console.log('=== Неверный тип ===')
    renderComponent({ title: 'Тест', count: 'не число' })
    
    console.log('=== Неверное значение validator ===')
    renderComponent({ title: 'Тест', status: 'unknown', size: 'xxl' })
    
    console.log('=== Дефолтные значения (массив — фабрика) ===')
    const r1 = validate({ title: 'A' })
    const r2 = validate({ title: 'B' })
    console.log('Разные массивы?', r1.props.tags !== r2.props.tags)  // true — не shared ref