vuejs / core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
https://vuejs.org/
MIT License
47.22k stars 8.26k forks source link

PropsFor inferring from DefineComponent variants #5612

Open cefn opened 2 years ago

cefn commented 2 years ago

What problem does this feature solve?

Within utilities, it can be useful to be able to type the props of a component. For example, a @vue/test-utils mount call could look like this...

describe('My Component', () => {
  async function render(extraProps?: Partial<PropsFor<typeof MyComponent>>) => {
    return mount(MyComponent, {
      propsData: {
        flavour:"vanilla",
        sprinkles:true,
        ...extraProps,
      },
    });
  };

I successfully established PropsFor from a reference to MyComponent (an example of a DefineComponent which used the Options API) like this...

export type PropsFor<T extends DefineComponent<any, any, any>> =
  T extends DefineComponent<infer Def, any, any>
    ? ExtractPropTypes<Def>
    : never;

I feel it's impractical for anyone outside the project to maintain such a definition. Matching one defineComponent signature seems brittle and perhaps depends closely on the overload sequence of defineComponent. For example as soon as I moved away from Options API and start using setup to implement MyComponent it seems the above implementation breaks.

Also the Vue project may well need to add further defineComponent signatures over time and it's infeasible to track these changes.

Ideally there would be a canonical representation of PropsFor which is maintained within the Vue3 repository, so that you can derive the type of Props given any typed reference to a component.

What does the proposed API look like?

type MyProps = PropsFor<typeof MyComponent>

...where MyComponent is defined like...

export default defineComponent({
  name: 'MyComponent',
  [...]
})
posva commented 2 years ago

@pikax Didn't you work on a PR that made component definition types canonical to enable this kind of usage?

pikax commented 2 years ago

@pikax Didn't you work on a PR that made component definition types canonical to enable this kind of usage?

I did, but I think is different than what's being requested here, I worked on passing props definition to the component, but because of the typescript limitation of not supporting partial typing on generics.


I think this issue is valid we need those helpers to be provided, not sure about the name but don't have a strong opinion

cefn commented 2 years ago

Update: We are still battling with being unable to consistently establish the props of components in our test suites. Is there an option I've missed somewhere?

Within a test we need to be able to author functions that take multiple different components.

For example, our components often need to be tested as a descendant of an ancestor that implements injection, and which has render-exception handling.

The wrap in this ancestor utility function inevitably accepts a component and its props. However, because of the complexity of Vue component typing, we can't even establish the type of their props from a component definition.

dpschen commented 2 years ago

@cefn: Not sure, but maybe volars vue-component-meta can help.

cefn commented 2 years ago

Hey, thanks for the suggestion.

Having looked at it I feel Volar's tooling nature (consuming the source code and reasoning over the abstract syntax tree) makes it quite a different category than type inference. It will introduce substantial complexity and unnecessary failure modes and could certainly never be used at runtime (although my own primary use case is testing, it's true).

We have been using our own pure-typing props inference definition for 6 months or so but it's effective only for our own components and when we start to use a new Vue feature (e.g. emits) then it breaks since the shape we assume when inferring has to be different.

It's for this reason that an inference definition that reflects knowledge of the internal typing design of a Vue component is needed, and having it in the core means it would track changes.

dpschen commented 2 years ago

Makes sense. I did some more digging and found this in volar:

export type ComponentProps<T> =
    ${vueCompilerOptions.strictTemplates ? '' : 'Record<string, unknown> &'}
    GlobalAttrs &
    ExtractProps<T>;

(source)

export type ExtractProps<T> =
    T extends FunctionalComponent<infer P> ? P
    : T extends new (...args: any) => { $props: infer Props } ? Props
    : T; // IntrinsicElement

(source)