core icon indicating copy to clipboard operation
core copied to clipboard

types(defineComponent): Stricter Component Type + helpers

Open pikax opened this issue 2 years ago • 20 comments

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

  • https://github.com/vuejs/docs/pull/2577
  • https://github.com/vuejs/test-utils/pull/2242
  • https://github.com/vuejs/language-tools/pull/3731
  • https://github.com/vuejs/router/pull/2039
  • https://github.com/vuetifyjs/vuetify/pull/18785
  • https://github.com/vueuse/vueuse/pull/3574

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.

pikax avatar Nov 06 '23 16:11 pikax

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

github-actions[bot] avatar Nov 06 '23 16:11 github-actions[bot]

/ecosystem-ci run

pikax avatar Nov 08 '23 09:11 pikax

📝 Ran ecosystem CI: Open

suite result latest scheduled
language-tools :x: failure :x: failure
nuxt :x: failure :white_check_mark: success
pinia :x: failure :white_check_mark: success
quasar :x: failure :white_check_mark: success
router :x: failure :white_check_mark: success
test-utils :x: failure :x: failure
vant :x: failure :white_check_mark: success
vite-plugin-vue :x: failure :white_check_mark: success
vitepress :x: failure :white_check_mark: success
vue-i18n :x: failure :white_check_mark: success
vue-macros :x: failure :white_check_mark: success
vuetify :x: failure :white_check_mark: success
vueuse :x: failure :x: failure
vue-simple-compiler :x: failure :white_check_mark: success

vue-bot avatar Nov 08 '23 09:11 vue-bot

📝 Ran ecosystem CI: Open

suite result latest scheduled
language-tools :x: failure :x: failure
nuxt :x: failure :white_check_mark: success
pinia :white_check_mark: success :white_check_mark: success
quasar :white_check_mark: success :white_check_mark: success
router :x: failure :white_check_mark: success
test-utils :x: failure :x: failure
vant :x: failure :white_check_mark: success
vite-plugin-vue :white_check_mark: success :white_check_mark: success
vitepress :x: failure :white_check_mark: success
vue-i18n :white_check_mark: success :white_check_mark: success
vue-macros :x: failure :white_check_mark: success
vuetify :x: failure :white_check_mark: success
vueuse :x: failure :x: failure
vue-simple-compiler :white_check_mark: success :white_check_mark: success

vue-bot avatar Nov 08 '23 13:11 vue-bot

/ecosystem-ci run

pikax avatar Nov 09 '23 07:11 pikax

📝 Ran ecosystem CI: Open

suite result latest scheduled
language-tools :x: failure :x: failure
nuxt :x: failure :white_check_mark: success
pinia :x: failure :white_check_mark: success
quasar :white_check_mark: success :white_check_mark: success
router :x: failure :white_check_mark: success
test-utils :x: failure :x: failure
vant :white_check_mark: success :white_check_mark: success
vite-plugin-vue :white_check_mark: success :white_check_mark: success
vitepress :x: failure :white_check_mark: success
vue-i18n :white_check_mark: success :white_check_mark: success
vue-macros :x: failure :white_check_mark: success
vuetify :x: failure :white_check_mark: success
vueuse :x: failure :white_check_mark: success
vue-simple-compiler :white_check_mark: success :white_check_mark: success

vue-bot avatar Nov 09 '23 07:11 vue-bot

Deploy Preview for vue-sfc-playground failed.

Name Link
Latest commit 1d7ef7f6848bd08a880d76045968b3cc75b7b022
Latest deploy log https://app.netlify.com/sites/vue-sfc-playground/deploys/656203f0796cc20008f0ede8

netlify[bot] avatar Nov 13 '23 15:11 netlify[bot]

/ecosystem-ci run

pikax avatar Nov 14 '23 08:11 pikax

📝 Ran ecosystem CI: Open

suite result latest scheduled
language-tools :x: failure :x: failure
nuxt :x: failure :white_check_mark: success
pinia :white_check_mark: success :white_check_mark: success
quasar :white_check_mark: success :x: failure
router :x: failure :white_check_mark: success
test-utils :x: failure :x: failure
vant :x: failure :x: failure
vite-plugin-vue :white_check_mark: success :white_check_mark: success
vitepress :x: failure :white_check_mark: success
vue-i18n :white_check_mark: success :white_check_mark: success
vue-macros :x: failure :white_check_mark: success
vuetify :x: failure :x: failure
vueuse :x: failure :white_check_mark: success
vue-simple-compiler :white_check_mark: success :white_check_mark: success

vue-bot avatar Nov 14 '23 09:11 vue-bot

📝 Ran ecosystem CI: Open

suite result latest scheduled
language-tools :x: failure :x: failure
nuxt :white_check_mark: success :white_check_mark: success
pinia :white_check_mark: success :white_check_mark: success
quasar :white_check_mark: success :white_check_mark: success
router :white_check_mark: success :white_check_mark: success
test-utils :x: failure :white_check_mark: success
vant :white_check_mark: success :white_check_mark: success
vite-plugin-vue :white_check_mark: success :white_check_mark: success
vitepress :white_check_mark: success :white_check_mark: success
vue-i18n :white_check_mark: success :white_check_mark: success
vue-macros :x: failure :white_check_mark: success
vuetify :x: failure :x: failure
vueuse :x: failure :white_check_mark: success
vue-simple-compiler :white_check_mark: success :white_check_mark: success

vue-bot avatar Nov 18 '23 11:11 vue-bot

📝 Ran ecosystem CI: Open

suite result latest scheduled
language-tools :x: failure :x: failure
nuxt :x: failure :white_check_mark: success
pinia :white_check_mark: success :white_check_mark: success
quasar :white_check_mark: success :white_check_mark: success
router :white_check_mark: success :white_check_mark: success
test-utils :x: failure :white_check_mark: success
vant :white_check_mark: success :white_check_mark: success
vite-plugin-vue :white_check_mark: success :white_check_mark: success
vitepress :white_check_mark: success :white_check_mark: success
vue-i18n :white_check_mark: success :white_check_mark: success
vue-macros :x: failure :white_check_mark: success
vuetify :x: failure :x: failure
vueuse :x: failure :white_check_mark: success
vue-simple-compiler :white_check_mark: success :white_check_mark: success

vue-bot avatar Nov 22 '23 13:11 vue-bot

📝 Ran ecosystem CI: Open

suite result latest scheduled
language-tools :x: failure :x: failure
nuxt :x: failure :white_check_mark: success
pinia :x: failure :white_check_mark: success
quasar :x: failure :white_check_mark: success
router :x: failure :white_check_mark: success
test-utils :x: failure :white_check_mark: success
vant :x: failure :white_check_mark: success
vite-plugin-vue :x: failure :white_check_mark: success
vitepress :x: failure :white_check_mark: success
vue-i18n :stop_button: cancelled :white_check_mark: success
vue-macros :x: failure :white_check_mark: success
vuetify :x: failure :white_check_mark: success
vueuse :x: failure :white_check_mark: success
vue-simple-compiler :x: failure :white_check_mark: success

vue-bot avatar Nov 25 '23 14:11 vue-bot

/ecosystem-ci run

yyx990803 avatar Jan 04 '24 06:01 yyx990803

📝 Ran ecosystem CI: Open

suite result latest scheduled
language-tools :x: failure :x: failure
nuxt :x: failure :white_check_mark: success
pinia :white_check_mark: success :white_check_mark: success
quasar :white_check_mark: success :white_check_mark: success
router :x: failure :white_check_mark: success
test-utils :x: failure :white_check_mark: success
vant :white_check_mark: success :white_check_mark: success
vite-plugin-vue :x: failure :white_check_mark: success
vitepress :white_check_mark: success :white_check_mark: success
vue-i18n :white_check_mark: success :white_check_mark: success
vue-macros :x: failure :x: failure
vuetify :x: failure :x: failure
vueuse :x: failure :white_check_mark: success
vue-simple-compiler :white_check_mark: success :white_check_mark: success

vue-bot avatar Jan 04 '24 06:01 vue-bot

Will this help resolve question I asked? https://github.com/radix-vue/shadcn-vue/issues/277

Thanks for TS Magic 🪄

jd-solanki avatar Jan 16 '24 09:01 jd-solanki

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.

pikax avatar Jan 16 '24 20:01 pikax

@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 :pray:

sadeghbarati avatar Feb 29 '24 20:02 sadeghbarati

@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

pikax avatar Feb 29 '24 20:02 pikax

@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

  • https://github.com/wobsoriano/svelte-sonner/blob/main/src/lib/state.ts
  • https://github.com/wobsoriano/svelte-sonner/blob/main/src/lib/types.ts

sadeghbarati avatar Feb 29 '24 20:02 sadeghbarati

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

daaanny90 avatar Sep 24 '24 14:09 daaanny90

Hi, first of all thanks a lot for your work @pikax ❤️ Is there any plan on how to contine with this change? E.g. a roadmap or a release it's planned for. It looks like it's been stale for quiet a while and in my opinion it could bring a huge benefit to the ecosystem.

markbrockhoff avatar Feb 04 '25 14:02 markbrockhoff