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

types(defineComponent): Stricter Component Type + helpers #9556

Open
wants to merge 83 commits into
base: minor
Choose a base branch
from

Conversation

pikax
Copy link
Member

@pikax pikax commented Nov 6, 2023

closes #7259 #9296

Breaking Changes

This PR causes breaking changes, because the types become more stricter on Component in general, this is necessary to prevent false type safety by preventing the DefineComponent and Component from being any.

Ecosystem

There's companion PRs for the ecosystem

Preliminary work as been done, once 3.5 alpha is released I'll update the PRs to add the alpha as dependency.

I expect Component libraries to be broken, please reach out to me through Discord on #typescript channel, tagging @pikax.

Details

Added the ability to extract the original options which the component was created:

const comp = defineComponent({
  randomOption: 'option',
})
expectType<string>(comp.randomOption)

This will be used to build the component types helpers

Improvements

defineComponent

defineComponent has become more stricter, now is possible to extract component options as they were passed

const comp = defineComponent({ myFoo: { a: 1 } })
comp.myFoo // { a: 1 }

defineAsyncComponent

defineAsyncComponent got it's types improved now correctly exposes it's own type as options and on rendering the innerComponent.

const Comp = defineAsyncComponent(async  ()=> defineComponent( { name: 'myComp'}));

Comp.name // 'AsyncComponentWrapper'
Comp.__asyncResolved // DefineComponent | undefined

defineCustomElement

defineCustomElement improvement on resolving the props for passed components, it should correctly infer props from passed components.


Helpers

There's 2 types of helpers introduced, Extract* and Component* , the Extract will extract the original type passed to define the component, the Component will provide the typescript type.

Extract can be used to enhanced the component options, eg: add more props

  • ExtractComponentOptions
  • ExtractComponentProp
  • ExtractComponentSlots
  • ExtractComponentEmits
  • ComponentProps
  • ComponentSlots
  • ComponentEmits

ExtractComponentOptions

Extract the original options from the component.

NOTE: It does not work with functional components, because it causes the Generics information to be lost.

const options = {
 props: { foo: String },
 setup(){
   // ...
 }
}

const Comp = defineComponent(options)
expectType<ExtractComponentOptions<typeof Comp>>(options)

ExtractComponentProp

Extracts the object passed to create options or if options are not available it will provide the typescript object definition.

const options = {
 props: { foo: String },
 setup(){
   // ...
 }
}

const Comp = defineComponent(options)
expectType<ExtractComponentProp<typeof Comp>>(options.props)

// if the original options are not provided, eg: Volar or functional components
const Comp = defineComponent((props: { a: 1 }) => () => {})
expectType<ExtractComponentProp<typeof Comp>>({ a: 1 as 1 })
// @ts-expect-error not valid type
expectType<ExtractComponentProp<typeof Comp>>({ a: 2 /* the type is { a: 1 } */ })

ExtractComponentSlots

Extracts original slot options from the component, if no options are available it won't be able to infer them, since slots are not exposed on functional components, that information is lost on defineComponent

const options = {
  slots: {
    default(arg: { msg: string }) {}
  },
  setup() {
    // ...
  }
}

const Comp = defineComponent(options)
expectType<ExtractComponentSlots<typeof Comp>>(options.slots)

ExtractComponentEmits

Extracts original emits options from the component, if no options are available it won't be able to infer them, since emits are not exposed on functional components, that information is lost on defineComponent. You can use ComponentProps to get the runtime type, props on functional components includes on*.

const options = {
  emits: {
    foo: (payload: { bar: number }) => true
  },
  setup() {
    // ...
  }
}

const Comp = defineComponent(options)
expectType<ExtractComponentEmits<typeof Comp>>(options.emits)

ComponentProps

Provides the typescript representation of component props, it appends emits props by default.

const options = {
  props: { foo: String },
  setup() {
    // ...
  }
}

const Comp = defineComponent(options)
expectType<{
  foo?: string | undefined
}>({} as ComponentProps<typeof Comp>)

// with emits
const optionsEmits = {
  props: { modelValue: String },
  emits: {
    'update:modelValue': (payload: string) => true
  },
  setup() {
    // ...
  }
}

const Comp = defineComponent(optionsEmits)
expectType<{
  foo?: string | undefined
  'onUpdate:modelValue'?: (payload: string) => void
}>({} as ComponentProps<typeof Comp>)

// exclude emits
const a = {} as ComponentProps<typeof Comp, true>
// @ts-expect-error
a['onUpdate:modelValue']

ComponentSlots

const optionsSlots = {
  slots: {} as {
    test: (payload: string) => any
  },
  setup() {
    // ...
  }
}

const Comp = defineComponent(optionsSlots)

expectType<{
  test: (payload: string) => any
}>({} as ComponentSlots<typeof Comp>)

ComponentEmits

DeclareComponent

DeclareComponent is an helper to create Vue components through types without needing to pass 16 arguments to the type.

It contains 5 arguments:

export type DeclareComponent<
  Props extends Record<string, any> | { new (): ComponentPublicInstance } = {},
  Data extends Record<string, any> = {},
  Emits extends EmitsOptions = {},
  Slots extends SlotsType = {},
  Options extends Record<PropertyKey, any> = {},
>

Props

It allows to pass just props object or a new Component generic, if a component is passed it will short-circuit the type.

Data

Data exposed publicly when using template :ref.

Options

Allowing to Provide custom properties to the Options object.

Copy link

github-actions bot commented Nov 6, 2023

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 89.3 kB 34 kB 30.6 kB
vue.global.prod.js 146 kB 53.2 kB 47.6 kB

Usages

Name Size Gzip Brotli
createApp 49.7 kB 19.5 kB 17.8 kB
createSSRApp 53 kB 20.8 kB 19 kB
defineCustomElement 52 kB 20.2 kB 18.4 kB
overall 63.2 kB 24.4 kB 22.2 kB

@pikax pikax added the 🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. label Nov 8, 2023
@pikax

This comment was marked as outdated.

@vue-bot

This comment was marked as resolved.

@vue-bot

This comment was marked as outdated.

@pikax

This comment was marked as outdated.

@vue-bot

This comment was marked as outdated.

This comment was marked as outdated.

@pikax

This comment was marked as outdated.

@jd-solanki
Copy link

Will this help resolve question I asked? unovue/shadcn-vue#277

Thanks for TS Magic 🪄

@pikax
Copy link
Member Author

pikax commented Jan 16, 2024

Will this help resolve question I asked? radix-vue/shadcn-vue#277

@jd-solanki no, the issue you linked is related with the vue compiler being able to infer props like that, bear in mind the compiler must know all the available props ahead of time ,to be able to generate the vue props object. When you extend or implement the type might lose that information.

@sadeghbarati
Copy link

@pikax Hey!

Please check the Svelte type helpers ComponentProps, and ComponentType in this source

https://github.com/wobsoriano/svelte-sonner/blob/main/src/lib/types.ts#L30C2-L31C51

What is the equivalent of this source with your Vue type helpers?

Thanks and sorry for tag 🙏

@pikax
Copy link
Member Author

pikax commented Feb 29, 2024

@sadeghbarati what type are you looking for, the two lines you sent one is the component and the other is props, we have:

  • Component is ComponentType
  • ComponentProps is ComponentProps

@sadeghbarati
Copy link

sadeghbarati commented Feb 29, 2024

@pikax

I have something like this in Vue to get props, I have to use typeof keyword in ComponentProps right? but it doesn't work in types

export type ToastT<T extends Component = Component> =  {
  component?: T; // T is Vue component but how to get types without typeof 
  componentProps?: ComponentProps<typeof T>; // 'T' only refers to a type, but is being used as a value here.
}

Please take a look at svelte-sonner

@daaanny90
Copy link

Hey! Any news on this? It would be cool to use and test generics! Without this merge, vuejs/test-utils#2254 cannot be closed, and we cannot really use generics. Can I help in some way to move this forward?

@edison1105 edison1105 deleted the branch minor October 21, 2024 07:37
@edison1105 edison1105 closed this Oct 21, 2024
@edison1105 edison1105 deleted the pikax/component_helper_types branch October 21, 2024 07:37
@edison1105 edison1105 restored the pikax/component_helper_types branch October 21, 2024 08:14
@edison1105 edison1105 reopened this Oct 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🍰 p2-nice-to-have Priority 2: this is not breaking anything but nice to have it addressed. scope: types version: minor
Projects
Status: Needs Review
Status: Done
Development

Successfully merging this pull request may close these issues.

Add types Props<T>, Events<T> and Slots<T> which provide types for components
8 participants