How to Unlock the Full Potential of Prop Types in Vue

Are you using prop types to their full potential?

I know I'm not.

It's a part of Vue that's just glossed over.

But there's a ton of powerful stuff you can do with them.

So I collected 5 tips on getting the most out of prop types:

  1. Always Use Prop Types
  2. Restrict a Prop to a List of Values
  3. Test Array and Object Shapes
  4. Know When to Provide Default Values
  5. Don't Overload Your Prop Types

Update: I also wrote a tip on stealing prop types.

1. Always Use Prop Types

We'll start with the most basic one:

Use prop types in the first place!

Prop types are a really simple way to catch a lot of potential errors.

They let you describe what types of values your component is expecting for each prop. This reduces the chances of someone using your component the wrong way.

For example, let's use this ShoppingCart the wrong way:

<ShoppingCart total="This is wrong" />

If our ShoppingCart component accepts total as a Number, we want to prevent someone from passing something other than a Number. We can do this by adding in a prop type:

export default {
name: 'ShoppingCart',
props: {
total: Number,
}
}

Now if we try to pass in a string, or anything other than a Number, Vue will give us a warning.

We can do far more interesting things to validate props though.

2. Restrict a Prop to a List of Values

Let's say we've got an Image component, and we can set the style of the image to be one of:

  • square
  • rounded
  • circle

We'll use it like this:

<Image
src="this/cool/image.png"
style="square"
/>

And we'll add in prop types for both props, src and style, to make sure that only strings are passed in:

export default {
name: 'Image',
props: {
src: String,
style: String,
}
};

But while this prevents us from passing in a Number (or anything other than a string), we can still pass in whatever string we want to:

<Image
src="this/cool/image.png"
style="rectangle"
/>

Luckily, we can use a validator in our prop types. This is a function that takes in the prop and returns true or false if the prop is valid.

It looks something like this:

(prop) => return propIsValid;

So let's get started adding in this extra validation!

First we have to change our prop types to use an object, so we can add more details to it:

export default {
name: 'Image',
props: {
src: {
type: String,
},
style: {
type: String,
}
}
};

Then we'll add in the validator function for the style prop:

export default {
name: 'Image',
props: {
src: {
type: String,
},
style: {
type: String,
validator: (prop) => [
'square',
'rounded',
'circle',
].includes(prop)
}
}
};

The validator function is just this piece (formatted to fit better):

(prop) => ['square', 'rounded', 'circle'].includes(prop)

Now if you tried to pass in something other than what's in that list, Vue will give you a warning.

3. Test Array and Object Shapes

In a similar way, we can use a validator function to verify the shape of an Array or Object.

Here we have a simple TagBox component, which let's us add and remove tags for our blog posts:

<TagBox :tags="['javascript', 'frontend', 'css', 'vue']" />

The prop tags lets us pass in an array of strings, which each represent a single tag.

With the type property we can make sure that the tags prop is always an array:

export default {
name: 'TagBox',
props: {
tags: {
type: Array,
}
}
};

But in our TagBox component we also want to verify that each element of the array is a string. We don't want people passing in random objects, numbers, and other things.

How can we do this?

Well, we'll have to get a little creative with using Array methods in our validator function:

export default {
name: 'TagBox',
props: {
tags: {
type: Array,
validator: (prop) => prop.every(e => typeof e === 'string'),
}
}
};

The array method every runs a function against every single element in the array. It will return true only if every function returns true.

The function we run against each element is checking to see that each element is a string:

(e) => typeof e === 'string'

We could just as easily have checked that they were all numbers, dates, or any other type.

But lets take this one step further.

What if we want a minimum length for each tag?

We want to prevent someone from displaying super short tags with a and it, which will probably just look weird:

export default {
name: 'TagBox',
props: {
tags: {
type: Array,
validator: (prop) => prop.every(e =>
typeof e === 'string' &&
e.length > 3
),
}
}
};

Now our validator function looks like this:

(prop) => prop.every(e =>
typeof e === 'string' &&
e.length > 3
)

I just added in an extra condition to check the length of the string.

And it doesn't have to stop there.

You can take this as far as you want, checking all sorts of things about the elements and objects that are passed in as props.

However, if you start doing things much more complex than this, you'll probably want to use a library. There are a few out there, such as vue-types and prop-types.

4. Know When to Provide Default Values

Along with adding types to your props, you can also specify if the prop is required or not:

export default {
name: 'BirthdayBanner',
props: {
birthday: {
type: Date,
required: true,
}
}
}

As a rule, if a prop isn't required, it should have a default value.

If you aren't requiring the user to provide a value, and you aren't providing a value either, then who is?

// The user *has* to provide a value, so we are safe
props: {
birthday: {
type: Date,
required: true,
}
}
// The user may not provide a value, so we should just in case
props: {
birthday: {
type: Date,
default: new Date('July 10, 1856'),
}
}

The opposite is also true.

If you are requiring the user to provide a value, don't provide a default.

This default value will never be used, and will only cause confusion for those looking at your code later on. Best to keep it clean and remove anything that isn't necessary.

// So... the birthday will start out as July 10, 1856, right?
// Wait, whoever is using the component is providing the birthday.
// Why is that value even there then?
// Now I'm just confused... halp.
export default {
name: 'BirthdayBanner',
props: {
birthday: {
type: Date,
required: true,
// This value is never used, so get rid of it!
default: new Date('July 10, 1856'),
}
}
}

5. Don't Overload Your Prop Types

Here's something you may not have known:

You can have multiple types on your prop definitions:

export default {
name: 'Counter',
props: {
value: {
type: [String, Number],
}
}
}

This component will accept both strings and numbers as valid types, but nothing else.

While this is possible, I wouldn't recommend it in most cases. Keeping your types stricter will keep your code more focused, and leave less room for bugs to come up.

For example, if you're allowing this prop to be either a string or a number, you need to handle both of those cases. This means you'll have to check to see if it's a string, and then convert it to a number first.

This Counter component would probably look more like this:

<template>
<div class="counter">
{{ count }}
</div>
</template>
export default {
name: 'Counter',
props: {
value: {
type: [String, Number],
}
},
computed: {
count() {
if (typeof this.value === 'number') {
return parseInt(this.value, 10);
}
return this.value;
}
}
}

Instead, if you just accepted numbers, you greatly simplify the logic of the component. You can get rid of all the extra logic:

<template>
<div class="counter">
{{ value }}
</div>
</template>
export default {
name: 'Counter',
props: {
value: {
type: Number,
}
},
}

Although this is a just a toy example, I think it illustrates the point.

Any real components you're writing will likely be more complex. The headaches from dealing with this will also be worse.


If you learned something from this article, please take the time to share it so others can learn as well!

It also encourages me to keep writing more stuff like this 😊