How to validate props in Vue

Validating props in Vue ensures component reliability, provides better developer experience, and catches errors early by enforcing type constraints and custom validation rules. As the creator of CoreUI, a widely used open-source UI library, I’ve implemented comprehensive prop validation in thousands of Vue components to ensure API consistency, prevent runtime errors, and provide clear development feedback in enterprise component libraries. From my expertise, the most effective approach is to use Vue’s built-in prop validation with type checking and custom validators. This method provides runtime validation, clear error messages, and excellent development experience while maintaining component robustness and API reliability.

Use Vue’s prop validation with type checking, required constraints, and custom validators for reliable component APIs.

<template>
  <div class="user-card" :class="cardClasses">
    <div class="user-avatar">
      <img :src="avatar" :alt="name + ' avatar'" :style="avatarStyle">
    </div>
    <div class="user-info">
      <h3 :style="{ color: theme === 'dark' ? 'white' : 'black' }">{{ name }}</h3>
      <p class="user-role">{{ role }}</p>
      <div class="user-stats">
        <span>Age: {{ age }}</span>
        <span>Posts: {{ postsCount }}</span>
        <span>Score: {{ score }}</span>
      </div>
      <div class="user-status" :class="{ active: isActive }">
        Status: {{ status }}
      </div>
      <div class="user-tags">
        <span v-for="tag in tags" :key="tag" class="tag">{{ tag }}</span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'UserCard',
  props: {
    // 1. Basic type validation
    name: {
      type: String,
      required: true
    },

    // 2. Multiple types allowed
    age: {
      type: [Number, String],
      required: true,
      validator: value => {
        const numAge = typeof value === 'string' ? parseInt(value) : value
        return numAge >= 0 && numAge <= 150
      }
    },

    // 3. Object validation with custom validator
    user: {
      type: Object,
      validator: value => {
        // Ensure object has required properties
        return value &&
               typeof value.id === 'number' &&
               typeof value.email === 'string' &&
               value.email.includes('@')
      }
    },

    // 4. Array validation with element type checking
    tags: {
      type: Array,
      default: () => [],
      validator: arr => {
        // Ensure all elements are strings and not empty
        return arr.every(tag => typeof tag === 'string' && tag.trim().length > 0)
      }
    },

    // 5. Enum-like validation
    role: {
      type: String,
      default: 'user',
      validator: value => {
        return ['admin', 'moderator', 'user', 'guest'].includes(value)
      }
    },

    // 6. Number with range validation
    score: {
      type: Number,
      default: 0,
      validator: value => value >= 0 && value <= 100
    },

    // 7. String with pattern validation
    avatar: {
      type: String,
      default: 'https://via.placeholder.com/150',
      validator: url => {
        // Basic URL validation
        try {
          new URL(url)
          return true
        } catch {
          return false
        }
      }
    },

    // 8. Complex object validation
    settings: {
      type: Object,
      default: () => ({
        theme: 'light',
        notifications: true,
        language: 'en'
      }),
      validator: settings => {
        const validThemes = ['light', 'dark', 'auto']
        const validLanguages = ['en', 'es', 'fr', 'de', 'it']

        return settings &&
               validThemes.includes(settings.theme) &&
               typeof settings.notifications === 'boolean' &&
               validLanguages.includes(settings.language)
      }
    },

    // 9. Function validation
    onClick: {
      type: Function,
      default: () => () => {}
    },

    // 10. Custom class validation
    size: {
      type: String,
      default: 'medium',
      validator: value => ['small', 'medium', 'large', 'xlarge'].includes(value)
    },

    // 11. Boolean with specific values
    isActive: {
      type: Boolean,
      default: false
    },

    // 12. Mixed validation with transformation
    postsCount: {
      type: [Number, String],
      default: 0,
      validator: value => {
        const num = typeof value === 'string' ? parseInt(value) : value
        return !isNaN(num) && num >= 0
      }
    },

    // 13. Date validation
    joinDate: {
      type: [Date, String],
      validator: value => {
        if (value instanceof Date) {
          return !isNaN(value.getTime())
        }
        if (typeof value === 'string') {
          const date = new Date(value)
          return !isNaN(date.getTime())
        }
        return false
      }
    },

    // 14. Custom type validation
    theme: {
      validator: value => {
        // Accept string themes or theme objects
        if (typeof value === 'string') {
          return ['light', 'dark', 'auto'].includes(value)
        }
        if (typeof value === 'object' && value !== null) {
          return value.primary && value.secondary && value.background
        }
        return false
      },
      default: 'light'
    },

    // 15. Conditional validation
    email: {
      type: String,
      validator: (value, props) => {
        // Email required if role is admin
        if (props && props.role === 'admin') {
          return value && value.includes('@') && value.includes('.')
        }
        // Optional for other roles, but if provided must be valid
        return !value || (value.includes('@') && value.includes('.'))
      }
    }
  },

  computed: {
    cardClasses() {
      return [
        `user-card--${this.size}`,
        `user-card--${typeof this.theme === 'string' ? this.theme : 'custom'}`,
        {
          'user-card--active': this.isActive
        }
      ]
    },

    avatarStyle() {
      const sizeMap = {
        small: '40px',
        medium: '60px',
        large: '80px',
        xlarge: '100px'
      }
      const size = sizeMap[this.size] || '60px'
      return {
        width: size,
        height: size,
        borderRadius: '50%'
      }
    },

    status() {
      return this.isActive ? 'Online' : 'Offline'
    }
  },

  // 16. Runtime validation with watchers
  watch: {
    score: {
      handler(newVal) {
        if (newVal < 0 || newVal > 100) {
          console.warn(`Invalid score: ${newVal}. Score must be between 0 and 100.`)
        }
      },
      immediate: true
    }
  },

  created() {
    // 17. Additional validation in lifecycle hooks
    this.validateProps()
  },

  methods: {
    validateProps() {
      // Custom validation logic
      if (this.role === 'admin' && !this.email) {
        console.warn('Admin users should have an email address')
      }

      if (this.tags.length > 10) {
        console.warn('Too many tags. Consider limiting to 10 or fewer.')
      }

      if (this.age && this.joinDate) {
        const joinYear = new Date(this.joinDate).getFullYear()
        const currentYear = new Date().getFullYear()
        const accountAge = currentYear - joinYear

        if (this.age < 13 && accountAge > 0) {
          console.warn('User may have been underage when they joined')
        }
      }
    }
  }
}
</script>

<!-- Usage examples with validation -->
<!--
Valid usage:
<UserCard
  name="John Doe"
  :age="25"
  role="admin"
  :score="85"
  :tags="['developer', 'vue']"
  email="[email protected]"
/>

Invalid usage (will show console warnings):
<UserCard
  name="John Doe"
  :age="-5"
  role="invalid-role"
  :score="150"
  :tags="[123, '']"
/>
-->

<style scoped>
.user-card {
  display: flex;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  background: white;
  transition: all 0.3s ease;
}

.user-card--small .user-info h3 { font-size: 14px; }
.user-card--medium .user-info h3 { font-size: 18px; }
.user-card--large .user-info h3 { font-size: 22px; }
.user-card--xlarge .user-info h3 { font-size: 26px; }

.user-card--dark {
  background: #333;
  color: white;
  border-color: #555;
}

.user-card--active {
  border-color: #4CAF50;
  box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}

.user-info {
  margin-left: 16px;
  flex: 1;
}

.user-stats span {
  margin-right: 12px;
  font-size: 14px;
  color: #666;
}

.user-status.active {
  color: #4CAF50;
  font-weight: bold;
}

.tag {
  display: inline-block;
  padding: 2px 8px;
  margin: 2px;
  background: #e3f2fd;
  border-radius: 12px;
  font-size: 12px;
  color: #1976d2;
}
</style>

Vue prop validation uses the validator function to implement custom validation logic that returns true for valid values and false for invalid ones. Combine type checking with custom validators for comprehensive validation. Use required constraints for mandatory props and provide sensible defaults for optional ones. Validation only runs in development mode for performance, so don’t rely on it for security. For complex validation, consider using schema validation libraries like Joi or Yup. Always provide clear error messages and fallback values for invalid props.

Best Practice Note:

This is the same approach we use in CoreUI Vue components for robust prop validation and excellent developer experience. Define clear prop contracts with proper types and validators, provide meaningful default values, validate early and fail fast in development, document validation rules in component documentation, and consider using TypeScript for additional compile-time type safety.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author