Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide way to properly type props/attrs in TS when not using props: option #1155

Closed
LinusBorg opened this issue May 10, 2020 · 7 comments
Closed
Labels
✨ feature request New feature or request transferred to rfc discussions Please manually search in http://github.com/vuejs/rfcs/discussions for the latest discussions

Comments

@LinusBorg
Copy link
Member

LinusBorg commented May 10, 2020

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.

@LinusBorg LinusBorg changed the title Provide Way to properly type props/attrs in TS when not using props: option Provide way to properly type props/attrs in TS when not using props: option May 10, 2020
@LinusBorg LinusBorg added the ✨ feature request New feature or request label May 10, 2020
@blocka
Copy link

blocka commented May 20, 2020

Are attrs automatically available in the template like props are?

@LinusBorg
Copy link
Member Author

they are through $attrs like today in Vue 2.

@PrettyWood
Copy link

PrettyWood commented Jun 9, 2020

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

@blocka
Copy link

blocka commented Jun 9, 2020 via email

@yyx990803
Copy link
Member

yyx990803 commented Jun 9, 2020

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.

@lxjwlt
Copy link

lxjwlt commented Aug 7, 2020

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>
    )
  }
})

@xialvjun
Copy link

xialvjun commented Dec 17, 2020

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>
    )
  }
})

@vuejs vuejs locked and limited conversation to collaborators Aug 31, 2023
@haoqunjiang haoqunjiang converted this issue into a discussion Aug 31, 2023
@haoqunjiang haoqunjiang added the transferred to rfc discussions Please manually search in http://github.com/vuejs/rfcs/discussions for the latest discussions label Sep 4, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
✨ feature request New feature or request transferred to rfc discussions Please manually search in http://github.com/vuejs/rfcs/discussions for the latest discussions
Projects
None yet
Development

No branches or pull requests

7 participants