Description
There 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.