Description
Summary
To be able to define component props with class properties. You can use prop
helper to specify detailed prop options:
import { Vue, prop } from 'vue-class-component'
// Define props in a class
class Props {
count = prop({
// Same as Vue core's prop option
type: Number,
required: true,
validator: (value) => value >= 0
})
}
// Pass the Props class to `Vue.with` so that the props are defined in the component
export default class MyComp extends Vue.with(Props) {}
In TypeScript, you can omit prop
helper when you only need to define its type (runtime validation does not happen in that case):
import { Vue, prop } from 'vue-class-component'
class Props {
// optional prop
foo?: string
// required prop
bar!: string
// optional prop with default
baz = prop<string>({ default: 'default value' })
}
export default class MyComp extends Vue.with(Props) {}
You need to specify "useDefineForClassFields": true
for TypeScript compiler option to let Vue Class Component aware of the properties without initializer (in the above example foo
and bar
):
{
"compilerOptions": {
"useDefineForClassFields": true
}
}
Motivation
One motivation is to properly type Props
type parameter of a component for props type checking in TSX and Vetur. TSX can validate props type on compile type thanks to TypeScript:
import { defineComponent } from 'vue'
// The type Props = { count: number } in component type
const Counter = defineComponent({
props: {
count: {
type: Number,
required: true
}
}
})
<Counter count={'Hello'} /> // Error because `count` is of type `number`
Vetur also offers similar prop type validation on <template>
block. To utilize these features, we need to properly type Props
type parameter of a component.
The other motivation is less verbosity. Vue's basic props
option requires us to define props with values then infers the prop type from the value. For example, we have to annotate complex type with PropType
utility:
interface Person {
firstName: string
lastName: string
}
const App = defineComponent({
props: {
// Specify value `Object` then annotate it with `PropType<Person>`
person: Object as PropType<Person>
}
})
This is relatively verbose compared to the existing @Prop
decorator approach from vue-property-decorator.
interface Person {
firstName: string
lastName: string
}
@Component
class App extends Vue {
// Just specify `Person` type (and `@Prop` decorator)
@Prop person: Person
}
Ideally, the new approach should as short as the decorator approach.
Details
We will introduce two API: Vue.with
static method and prop
helper function.
Vue.with(...)
method receives a class constructor that describes the component props. It collects all class properties and generates props
option for the component under the hood. It also respects the property types for the props types:
import { Vue } from 'vue-class-component'
class Props {
optional?: string
required!: number
}
class App extends Vue.with(Props) {
// Vue.with generates the following props option under the hood
// props: { optional: null, required: null }
mounted() {
// It retains the property types for props
this.optional // string | undefined
this.required // number
}
}
Note that we have to specify useDefineForClassFields: true
component option in TypeScript to make the above code works.
We can also specify detailed prop options by using prop
helper (e.g. default
, validator
). The prop
helper receives exact same as Vue core's props
option object:
class Props {
// with validator
count: number = prop({
validator: (count: number) => count >= 0
})
// with default
// You can specify the type via `prop` type parameter
amount = prop<number>({ default: 1 })
}
Note that we have to specify the type of prop via prop
helper type parameter when we use default
value. This is to differentiate required
prop and with-default
prop on the type level. That is, required
should be always of type string
but withDefault
should be of type string
in the component while being of type string | undefined
when it is used on a parent component since it does not have to receive a value. If the type is able to be inferred from the default value, you don't have to specify it.
class Props {
// type is `string`
required!: string
// type is `WithDefault<string>`
withDefault = prop({ default: 'default' })
}
class App extends Vue.with(Props) {
mounted() {
this.required // string
this.withDefault // string
}
}
// In the usage of TSX/template
// required: string
// withDefault: string | undefined
<App required="Hello" />
Alternative approaches
Decorator approach
There has been an approach with @Prop
decorator but there are issues which already described in #447
TL;DR
- Cannot type the
Props
type parameter, then there is no way to check props type. - There are concerns regarding uncertainties of the spec.
Mixin approach
This is an approach proposed in #447. But it turned out too verbose compared to the decorator approach. There is also a feedback that defining it as a mixin is not intuitive.