Vue-next: Provide way to properly type props/attrs in TS when not using props: option

Created on 10 May 2020  Â·  7Comments  Â·  Source: vuejs/vue-next

What problem does this feature solve?

  1. When we don't define any props on our component, all the props/attrs passed to our component are in context.attrs This is defined in RFC 31. 2. In that case, setup()'s first argument, props, is an empty object. This is not explicitly defined in the RFC above, but can be kind of expected.
  2. However, defineComponent allows us to pass specific types for props through the first generic argument, but we can't set specific types for context.attrs - it's always a plain Data type unless I manually typecast it in setup, which I find to be less than ideal.

Example:

type TabsProps = {
  modelValue: string
}
export const Tabs = defineComponent<TabsProps>({
  name: 'Tabs',
  //props: is not set,
  ,inheritAttrs: false,
  setup(props, { slots, attrs }) {
    console.log(JSON.stringify(props, null, 2)) // => {}
    console.log(JSON.stringify(attrs, null, 2)) // => { "modelValue": "some string" }

    // in the following lines, wrapProp() is a generic helper function that uses `keyof`  to wrap a specific prop in a typesafe way.

    // 1. Using `props`: TS is satisfied, as to TS, there's a `modelValue prop`
    //    but it fails but fails at runtime as `props` is really an empty object
    const state = wrapProp(props, 'modelValue') 

    // 2. Using `attrs`: this works at runtime, 
    //    but for TS, `state` is now ComputedRef<unknown>, as the type of the prop could not be inferred from `Data`
    const state = wrapProp(attrs, 'modelValue')  

    // do something with `state` here, i.e. pass it to a composable....

    return () => h(props.tag ?? 'DIV', slots.default?.())
  },
})

What does the proposed API look like?

We have two possible solutions here:

  1. We change the runtime-behaviour so that when no props: options are specified, the props argument contains all of the attributes, which means it contains the same content as attrs.
  2. We change the type definition of defineComponent in a way that we can type attrs like we can now type props.

I personally prefer the first way - if no props: options are defined, all attrs are also possibly props at the same time, so to me it makes sense that both contain the same references.

enhancement

Most helpful comment

defineComponent({
  props: {
    title:{
      type: String,
      required: true,
    },
  },
  setup: (props) => {
    return () => (
      <p>{props.title}</p>
    )
  }
})

code like above is tedious.

I think this is better:

defineComponent<{title:string, subTitle?: string}>({
  props: ['title', 'subTitle'], // this is to tell the difference between props and attrs
  setup: (props) => {
    return () => (
      <p>{props.title} <span>{props.subTitle}</span></p>
    )
  }
})

All 7 comments

Are attrs automatically available in the template like props are?

they are through $attrs like today in Vue 2.

Hi! Any feedback on this issue? I have some components that are basically just registries with some logic and I would love to be able to just use props in setup instead of duplicating the props option everywhere or using attrs, which becomes confusing in this case

How would type checking work in the templates if it's using attrs instead
of props

On Tue, Jun 9, 2020, 8:28 PM PrettyWood notifications@github.com wrote:

Hi! Any feedback on this issue? I have some components that are basically
just registries with some logic and I would love to be able to just use
props in setup instead of duplicating the props option everywhere

—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/vuejs/vue-next/issues/1155#issuecomment-641462562,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAAKROQKCKLMUOXRVWGJVJDRVZWLDANCNFSM4M5GK5ZA
.

This is actually close to what Optional Props Declaration is, and has the same problem when dealing with attrs fallthrough:

  • No fallthrough -> use class and style on the component but find them not applied to root, confusing

  • Fallthrough -> all the props you intend to use for state only are also applied to root as attributes. Also confusing.

Essentially, if we expose attrs as props when no props options are declared, it leads to ambiguous behavior for attr fallthrough.

I notice you are using inheritAttrs: false because you are aware of the problem above, but making props behave the same as attrs in this case makes it too easy for users to use it without being aware of the issue and later get surprised when they find out about it.

Another issue here is that in the current implementation, attrs is not reactive, because they are supposed to be only used in the render function and not as reactive state. If you want to do reactive work with something, you should declare it as props.

In this case, I also don't really see a reason to not use the props option, since that would give the prop type inference. The only reason I guess is preferring to declare props using TS types - which, while tempting, unfortunately doesn't provide enough runtime information to Vue to avoid ambiguity.

To solve that, we may need to introduce a separate option, e.g. optionalProps: true, which:

  • Implies inhertiAttrs: false (or, allows class, style and v-on fallthrough just like functional components)
  • Does not proxy props directly on this
  • Exposes everything received as props in setup function, as $props in templates, and as this.$props in options API. Also reactive.

not in runtime, compile to "props" by loader instead, is it better?

export interface PropsType {
  title: string;
}
let ShorterWay = defineComponent<PropsType>((props) => {
  return () => (
    <p>{props.title}</p>
  )
})

// compile to 
export interface PropsType {
  title: string;
}
let longWay = defineComponent({
  props: {
    title:String,
  },
  setup: (props) => {
    return () => (
      <p>{props.title}</p>
    )
  }
})
defineComponent({
  props: {
    title:{
      type: String,
      required: true,
    },
  },
  setup: (props) => {
    return () => (
      <p>{props.title}</p>
    )
  }
})

code like above is tedious.

I think this is better:

defineComponent<{title:string, subTitle?: string}>({
  props: ['title', 'subTitle'], // this is to tell the difference between props and attrs
  setup: (props) => {
    return () => (
      <p>{props.title} <span>{props.subTitle}</span></p>
    )
  }
})
Was this page helpful?
0 / 5 - 0 ratings

Related issues

mika76 picture mika76  Â·  3Comments

ConradSollitt picture ConradSollitt  Â·  4Comments

cexbrayat picture cexbrayat  Â·  4Comments

Jexordexan picture Jexordexan  Â·  4Comments

adamberecz picture adamberecz  Â·  3Comments