New way to define `props` and `emits` options
See original GitHub issueThere is an alternative idea here. Feedback welcome!
Summary
- To provide
props
andemits
helper function to define corresponding component options in class syntax with type safety.
import { props, emits, mixins } from 'vue-class-component'
// Define component props.
// Props is a class component mixin.
const Props = props({
value: {
type: Number,
required: true
}
})
// Define component emits.
// Emits is a class component mixin.
const Emits = emits({
input: (value) => typeof value === 'number'
})
// Use the above options by extending them.
export default Counter extends mixins(Props, Emits) {
mounted() {
console.log(this.value)
this.$emit('input', 10)
}
}
Motivation
We need additional helpers to define props
and emits
because there are no corresponding concepts in class syntax. You can define them via @Options
decorator, but the problem of the decorator approach is that it does not properly type the component type.
@Options({
props: ['value'],
emits: ['input']
})
class MyComp extends Vue {
mounted() {
this.value // -> type error
this.$emit('change', 10) // -> no type error (expecting an error)
}
}
Because props
and emits
options modify the existing component types $props
and $emit
, and has runtime declaration (validator
, default
, etc.) in addition to types, we have to define them as a super class (mixins).
Details
To provide props
and emits
function. They receive as the same value as component props
and emits
options.
import { props, emits } from 'vue-class-component'
// prop names
props(['foo', 'bar'])
// props options object
props({
count: {
type: Number,
required: true,
validator: (value) => {
return value >= 0
}
}
})
// event names
emits(['change', 'input'])
// emits options object
emits({
input: (value) => typeof value === 'number'
})
They return a class component mixin so that you can use them with mixins
helper function:
import { props, emits, mixins } from 'vue-class-component'
// Define props and emits
const Props = props(['value'])
const Emits = emits(['input'])
// Use props and emits definition by extending them with mixins helper
class MyComp extends mixins(Props, Emits) {
mounted() {
console.log(this.value)
this.$emit('input', 10)
}
}
As they are just Vue constructors, you can just extend it if there are no other mixins to extend:
import { props } from 'vue-class-component'
// Define props
const Props = props(['value'])
// Just extending Props
class MyComp extends Props {
mounted() {
console.log(this.value)
}
}
Why not decorators?
There has been an approach to define props
with ES decorators.
@Component
class App extends Vue {
@prop({ type: Number }) value
}
But the decorator approach has several issues unresolved yet as stated in abandoned Class API RFC for Vue core. Let’s bring them here and take a closer look:
-
Generic argument still requires the runtime props option declaration - this results in a awkward, redundant double-declaration.
Since decorators do not modify the original class type, we cannot type
$props
type with them:class MyComp extends Vue { @prop value: number mounted() { this.value // number this.$props.value // *error } }
To properly type props, we have to pass a type parameter to the super class which is a redundant type declaration.
// 1. Types for $props interface Props { value: number } class App extends Vue<Props> { // 2. props declaration @prop value: number }
-
Using decorators creates a reliance on a stage-2 spec with a lot of uncertainties, especially when TypeScript’s current implementation is completely out of sync with the TC39 proposal.
Although the current decorators proposal is stage 2, TypeScript’s decorator implementation (
experimentalDecorators
) is still based on stage 1 spec. The current Babel decorator (@babel/plugin-proposal-decorators
) is based on stage 2 but there is still uncertainty on the spec as the current spec (static decorator) is already different from the original stage 2 proposal (there is a PR to add this syntax in Babel), and also there is another proposal called read/write trapping decorators due to an issue in the static decorators.Vue Class Component already uses decorators syntax with
@Component
(in v8@Options
) but it would be good not to rely on it too much in a new API because of its uncertainty and to reduce the impact of potential breaking changes in the future when we adapt Vue Class Component with the latest spec. -
In addition, there is no way to expose the types of props declared with decorators on this.$props, which breaks TSX support.
This is similar to the first problem. The Vue component type has
Props
type parameter that TSX uses for checking available props type. For example, let’s say your component has a type{ value: number }
as the props type, then the component type isComponent<{ value: number }>
(this is different from the actual Vue type but you can get the idea from it). TSX knows what kind of value should be passed for the component:// MyComp type is `Component<{ value: number }>` import { MyComp } from './Counter.vue' <MyComp value={42} /> // Pass the compilation as the value type matches with the prop type <MyComp value={'Hello'} /> // Produce a type error as the value is different from the prop type
It is impossible to define the props type like the above with decorators because decorators cannot intercept the original class types.
// MyComp type is fixed with Component<{}>, even though there is @prop decorator! class MyComp extends Vue { @prop value: number }
The above class component can have the property
value
of typenumber
but it is not defined as props but just a normal property on the type level. Therefore, the component type is likeComponent<{}>
which TSX cannot know about the props.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:18
- Comments:36 (15 by maintainers)
Top GitHub Comments
I agree that :
(Vue 3 : class-component)
(Vue 2 class-component + property-decorator)
indeed the new Vue version is so much more verbose, less readable, less understandable, and of course less intuitive and less logical for a TypeScript Developer. `
Thank you for your feedback.
@nicolidin
I’m answering your questions below:
Firstly
I’m introducing
prop
helper because of the following reasons:To differentiate
required
prop type and with-default
prop type.If we allow defining the default value by just assigning it to the property, the property type will be as same as
required
prop without an initializer:This is fine when we just use the prop in the component:
But when we use this component in a parent component, a problem occurs:
The above usage of
<HelloWorld>
component is correct - we pass the required propfoo
, don’t have to passbar
as it has the default value. But compile-time validation will fail because it thinks thebar
prop is also required because of its typestring
. To be able to omitbar
, we have to make its typestring | undefined
in the usage of the parent component.This causes because we don’t provide any hint on the type level whether the prop is
required
or with-default
. If we useprop
helper, we can provide a type hint for props:In this way, we can make
bar
’s typestring
in component whilestring | undefined
in the usage in a parent component.To provide a way to specify other prop options.
I think we need to provide a way to define props options as same as basic Vue even if we can omit them. For example, we may want to add
validator
option to validate the prop value in detail. Also, Babel users would still want to specifytype
,required
options.Secondly
Let me clarify runtime validation and compile-time validation. Runtime validation is the validation that Vue does that you see on the browser’s console. It needs to run your code to validate the props on the browser. On the other hand, compile-time validation is the validation that TypeScript does that you see on your IDE/editor as a red line errors and on CI as compilation errors. It validates the props without running the code.
What I would like to achieve with this proposal is compile-time props validation. So the code:
should provide a red line on our IDE/editor if the value passed to the
person
is mismatched with the defined prop type. Note that we also have to modify Vetur to achieve the compile-time props validation with class component but it will be easier to implement if we properly type$props
type with this proposal.By the way, if we use TSX, the compile-time props validation is already usable. You can try it on the test case in props-class branch.
Lastly
Yes, we don’t need to use
PropType
even with complex types in this approach. e.g.: