Class style `props` definition
See original GitHub issueSummary
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.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:28
- Comments:29 (7 by maintainers)
Top GitHub Comments
I, for one, am a very happy user of
vue-property-decorator
, and it works well:It makes code really readable / simple, with everything in the class definitions and not imported types. I understand the concerns with the uncertainty of the spec but we’d just have to move to the next major version of
vue-class-component
if the times comes. Wouldn’t already need to do so becauseOptions
is a decorator anyway?I would further add that:
vue-class-component
has 390k weekly downloads on NPM,vue-property-decorator
has 358k weekly downloads. Dirty math makes it that 92% ofvue-class-component
’s users also downloadvue-property-decorator
and most likely use@Prop
.I read the thread, the answers, the motivations of the proposal and I allow myself to react.
First of all, I am grateful for the work and efforts of this library. A big thank-you.
I recognize the potential benefit of this proposal but find that there are a few points which have been overlooked (or have been left out).
First of all from an object-oriented point of view using inheritance is just the wrong way. This is one of the SOLID principles (prefer composition to inheritance). The interface does not meet expectations but is more correct from an object-oriented point of view. This is also a breaking change impacting all the components.
I remind you that multiple inheritance is not possible in JS / TS. I have already developed components that have been able to take advantage of the inheritance. Example: StepOne, StepTwo, StepThree which inherits from StepAbstract (thus sharing properties and methods). It works perfectly in Vue. With your solution how to share methods through these components?
I would also like to know how the $ refs will be managed? For me the problem seems the same but the double inheritance is not possible.
In the arguments, there was a comparison with ReactJS (which uses interfaces to set properties). Another comparison, often forgotten, is Angular which allows to define @Input (equivalent to Prop) and which are type checked in the template by the angular service. So that seems technically possible, doesn’t it?
Defining props inside the class is still more visible and readable. Why ? Because we have an “entity”, an object that defines the behavior of the component in one place. If we do inheritance, it surprises and it makes the reading less “linear” because we have to think of an object tree. It’s just how the cognition charge works.
In the other thread, the decorators were questioned because they are in an uncertain state. This problem has been known from the start and represents a risk. This point should be handled by typescript / Babel and not by libraries. Also, if this is really a problem then you should no longer use a decorator. For me to stop using just one doesn’t really make sense.
For me, there is a huge implication in the new proposal. The pure and simple stop of the decorators. Personnaly, i am a huge fan of decorator because i found that solution elegant and works better with oriented object.