question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

New way to define `props` and `emits` options

See original GitHub issue

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.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:18
  • Comments:36 (15 by maintainers)

github_iconTop GitHub Comments

32reactions
nicolidincommented, Sep 19, 2020

I agree that :

(Vue 3 : class-component)

const Props = props({
  firstProp: {
    type: Number,
    required: true
  },
  secondProp: {
    type: Number,
    required: true
  }
})

export default Counter extends mixins(Props, Emits) {

}

(Vue 2 class-component + property-decorator)

export default Counter extends Vue {
  @Prop() firstProp!: number;
  @Prop() secondProp!: number;
}

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. `

9reactions
ktsncommented, Sep 27, 2020

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:

class Props {
  // Required prop
  // foo is of type string
  foo!: string

  // Has default value
  // bar is of type string too
  bar = 'default value'
}

This is fine when we just use the prop in the component:

class HelloWorld extends Vue.props(Props) {
  mounted() {
    console.log(this.foo) // foo cannot be undefined since it is required
    console.log(this.bar) // bar cannot be undefined because of default value
  }
}

But when we use this component in a parent component, a problem occurs:

<HelloWorld foo="Test" />

The above usage of <HelloWorld> component is correct - we pass the required prop foo, don’t have to pass bar as it has the default value. But compile-time validation will fail because it thinks the bar prop is also required because of its type string. To be able to omit bar, we have to make its type string | 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 use prop helper, we can provide a type hint for props:

class Props {
  // Required prop
  // foo is of type string as same as the first example
  foo!: string

  // Has default value
  // bar is of type WithDefault<string> so that TypeScript
  // can know the `bar` prop is different from `foo`
  bar = prop({ default: 'default value' })
}

In this way, we can make bar’s type string in component while string | 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 specify type, required options.

class Props {
  // Specifying detailed prop options for Vue's runtime props validation
  theme = prop({
    type: String,
    required: true,
    validator: theme => ['primary', 'secondary', 'danger'].includes(theme)
  })
}

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:

<MyComponent :person="new Person()"/>

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.:

interface Person {
  firstName: string
  lastName: string
}

class Props {
  person!: Person // Just specify the Person interface here
}

class App extends Vue.props(Props) {
  mounted() {
    this.person // is of type Person
  }
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

emits Option - Vue 3 Migration Guide
Vue 3 now offers an emits option, similar to the existing props option. This option can be used to define the events that...
Read more >
Understanding the new script setup with (defineProps ... - Netlify
The script setup feature ships with the defineProps and defineEmits APIs that make it possible for us to declare props and emits. They...
Read more >
A Guide to Vue $emit - How to Emit Custom Events in Vue
In Vue 3, we have 2 different ways to do this: ... track custom events for a component by defining the emits option...
Read more >
How to Use Props in Vue.js - freeCodeCamp
“Props” is a special keyword which stands for properties. It can be registered on a component to pass data from a parent component...
Read more >
The 101 guide to Script Setup in Vue 3 - VueDose
To declare options like props and emits we have to use so called compiler macros which are automatically available inside <script setup> ....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found