Skip to content

Class style props definition #465

Open
@ktsn

Description

@ktsn

Summary

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions