Skip to content

New way to define props and emits options #447

Closed
@ktsn

Description

@ktsn

There is an alternative idea here. Feedback welcome!

Summary

  • To provide props and emits 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 is Component<{ 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 type number but it is not defined as props but just a normal property on the type level. Therefore, the component type is like Component<{}> which TSX cannot know about the props.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions