Extracting Props from Vuetify Component in vue 3

518 Views Asked by At

I am trying to write my own wrapper for vuetify components, so I don't have to define the same props in every component. E.g. I want to create my own TextField, that has defaultProps, but can also accept all props, that would normally go to VTextField.

I was defining the script setup section like this:

<script lang="ts" setup>
import type { ExtractPropTypes } from 'vue';
import { computed } from 'vue';
import { VTextField } from 'vuetify/components';

type VTextProps = InstanceType<typeof VTextField>['$props'];
// type VTextProps = ExtractPropTypes<typeof VTextField>;

const props = defineProps<VTextProps>();
const emit = defineEmits(['update:modelValue']);

const value = computed({
  get() {
    return props.modelValue;
  },
  set(value) {
    emit('update:modelValue', value);
  },
});
</script>

I tried two different versions of extracting the props of VTextField:

type VTextProps = InstanceType<typeof VTextField>['$props'];
type VTextProps = ExtractPropTypes<typeof VTextField>;

But I cannot get both of them to work. In my IDE it seems to function, also the autocomplete works, however vite is throwing the following error:

[vite] Internal server error: [@vue/compiler-sfc] Unresolvable type reference or unsupported built-in utility type

I feel like, the defineProps function of vue cannot correctly parse this utility type. What would be your suggestion for a workaround for this, so I don't have to define my own types? It seems vuetify does not export its types...

Any idea or help is highly appreciated!

2

There are 2 best solutions below

1
Moritz Ringler On

I don't think current Vue (3.4) can use an extracted type to generated props. In the IDE, it will look like it is working, since the IDE does full type analysis, but then the component won't compile.

In the documentation on composition API with TypeScript, there is a section on syntax limitations that says:

because the type to runtime conversion is still AST-based, some complex types that require actual type analysis, e.g. conditional types, are not supported. You can use conditional types for the type of a single prop, but not the entire props object.


However, Vuetify 3 components all build their props through a factory function (see code). You can import the function and use it to generate a props object for defineProps(). Ironically, the factory function is not typed, so you still need to use the extracted type if you want autocomplete and type hints in your IDE:

import { ComponentObjectPropsOptions, ExtractPropTypes } from 'vue';
import { makeVTextFieldProps } from 'vuetify/lib/components/VTextField/VTextField.mjs'
import type { VTextField } from 'vuetify/lib/components/index.mjs'

type VTextFieldProps = InstanceType<typeof VTextField>['$props']
type MyTextFieldProps = ComponentObjectPropsOptions<VTextFieldProps>
const props = defineProps(makeVTextFieldProps() as MyTextFieldProps)

If you want to extend the props object, you can use the props factory function from Vuetify directly. But since you cannot use local variables in defineProps(), and you have to supply TS types manually, I would not use it directly in defineProps(), but rather a function that supplies types, too. Something like:

// put this into its own .ts file or in a regular script block next to the script setup block

import type { ComponentObjectPropsOptions, ExtractPropTypes } from 'vue';
import { propsFactory } from 'vuetify/lib/util/propsFactory.mjs'

function buildExtendingProps<B, E>(baseProps: B, extendedProps: E, source: any)
{
  const combinedProps = {...baseProps, ...extendedProps}
  const builder = propsFactory(combinedProps, source)
  return builder() as ComponentObjectPropsOptions<B & ExtractPropTypes<E>>
}

Then you can use it like this:

import { makeVTextFieldProps } from 'vuetify/lib/components/VTextField/VTextField.mjs'
import type { VTextField } from 'vuetify/lib/components/index.mjs'

type VTextFieldProps = InstanceType<typeof VTextField>['$props']

const props = defineProps(buildExtendingProps(
  makeVTextFieldProps() as VTextFieldProps,
  {
    foo: {type: String, default: 'foo'}, 
    bar: {type: Number}
  },
  'MyTextField'
))

Not sure how practicable this is, but there you go.

1
Guilherme Alexandrino On

Recently I faced the same problem with Vue 3.3.1 and Vuetify 3.4.7!

Apparently Vue dont accept very complex types like vuetify component $props

Per example, if you try to define $slots for VTextField just do it this way and it will work:

<script setup lang="ts">
import { VTextField } from 'vuetify/components/VTextField'

type TextFieldSlots = InstanceType<typeof VTextField>['$slots']

defineSlots<TextFieldSlots>()

</script>

We can try with $props:

...

type TextFieldProps = InstanceType<typeof VTextField>['$props']

interface Props {
  icon?: string
  externalLabel?: string
}

withDefaults(defineProps<Props & TextFieldProps>(), {
  // externalLabel: 'Default External Label'
})
</script>

I got an error message:

[@vue/compiler-sfc] Unresolvable type reference or unsupported built-in utility type...

To "fix" it, I created a interface and extend type and use /* @vue-ignore */

Very strange way to fix.. but if none of the other ways work you can try this one. Full code, example:

TEMPLATE

<template>
  <div>
    <div v-if="externalLabel">{{ externalLabel }}</div>
    <v-text-field v-bind="{ ...$attrs, ...$slots }" variant="outlined">
      <!-- Default Slot for Prepend-Inner -> feature: small icon and right margin -->
      <template v-if="icon" v-slot:prepend-inner>
        <v-icon size="small" class="mr-1">{{ icon }}</v-icon>
      </template>

      <!-- Slots  -->        
      <template v-for="(slotScopedName, _, index) of (Object.keys($slots) as [])" #[slotScopedName]="slotData" :key="index">
        <slot :name="slotScopedName" v-bind="(slotData as {})"></slot>
      </template>

    </v-text-field>
  </div>
</template>

SCRIPT

<script setup lang="ts">
import { VTextField } from 'vuetify/components/VTextField'

type TextFieldProps = InstanceType<typeof VTextField>['$props']
type TextFieldSlots = InstanceType<typeof VTextField>['$slots']

interface ITextFieldProps extends /* @vue-ignore */ TextFieldProps {}
interface Props {
  icon?: string
  externalLabel?: string
}

defineOptions({ inheritAttrs: false })
defineSlots<TextFieldSlots>()
withDefaults(defineProps<Props & ITextFieldProps>(), {
  // externalLabel: 'Default External Label'
})
</script>

IMG - Autocomplete working for $props
IMG - Autocomplete working for $slots