Learn how to enhance a Vue component by adding extra properties while having Typescript support in Vue 3

Attempting to enhance a Vue component from PrimeVue, I aim to introduce extra props while preserving the original components' typings and merging them with my new props.

For example, when dealing with the Button component that requires "label" to be of type string, any attempt to pass a numeric value will trigger an error in my IDE (VSCode + Vetur).

To extend the Button class into my custom component, BaseButton, I can successfully pass a numeric or any other data type to the label prop without encountering any issues thanks to the common pattern demonstrated below. Alongside upholding the original Button component's typings, I also want to merge it with my additional props like myProp: string as shown.

<template>
  <Button v-bind="$attrs" :label="myComputedLabel">
    <template v-for="(_, slot) of $slots" v-slot:[slot]="scope">
      <slot :name="slot" v-bind="scope"/>
    </template>
  </Button>
</template>

<script>
  export default defineComponent({
  name: 'BaseButton',
  props: {
    myProp: {
      type: String
    }
  },
  setup(props) {
    const myComputedLabel = computed(() => {
      return `Hello ${props.myProp}`;
    });
  }
 })
</script>

In my scenario, the 3rd party component is built in Vue using vanilla JS but incorporates an external Button.d.ts typings file. Whether this detail is crucial remains uncertain.

Answer №1

If you are utilizing Vue 3 with script setup, the following is how you can achieve it:

import Button from 'primevue/button';

type ButtonInstance = InstanceType<typeof Button>

defineProps<{
  label: ButtonInstance['$props']['label'] | number
}>()

To set up the function:

import { PropType } from 'vue'

defineComponent({
  props: {
    label: {
      type: [String, Number] as PropType<ButtonInstance['$props']['label'] | number>,
    }
  },
})

An alternative method to extract prop types include:

import Button from 'primevue/button'

type ExtractComponentProps<TComponent> =
  TComponent extends new () => {
    $props: infer P;
  }
    ? P
    : never

type ButtonTypes = ExtractComponentProps<typeof Button>

Answer №2

To access the information from the setup option of a component, refer to the props as the initial parameter within the setup function

type CustomButtonProps = Parameters<NonNullable<typeof Button.setup>>[0]

In simple terms, this is a Generic Type definition

type ComponentProperties<T extends { setup?: any }> = Parameters<NonNullable<T['setup']>>[0]

Example of how to use it in code:

type CustomButtonProps = ComponentProperties<typeof Button>

Answer №3

If you're looking for a more structured approach, consider using Typescript with Setup Composition API.

<script setup lang="ts">
import { computed } from "vue"
let props = defineProps<{ myProp: string | number }>()
const myComputedLabel = computed(() => {
   return `Hello ${props.myProp}`;
})
</script>

Answer №4

From my personal experience, there are various approaches to achieving this depending on the complexity of the type and the version of Vue being used.

script setup

With the introduction of defineProps, it has become easier to define prop types without the need for casting to PropType<T> and with less verbose definitions. In Vue 3.3+, support was added for complex types and imported types with some limitations.

Here's an example of extending ComponentB from ComponentA.

<script setup lang="ts">
import ComponentA from 'componentA.vue'

interface AdditionalProps {
  // ... define your props here
}

type Props = AdditionalProps & (typeof ComponentA)['$props']

const props = defineProps<Props>()

//You can also use `withDefaults` to define default values

</script>

<template>
  <ComponentA v-bind="props">
</template>

Additionally, utility types like Omit can be utilized to remove certain props from the extended component.

However, there are instances where this approach may not work as expected, especially when dealing with "conditional types". I faced challenges implementing this with Vuetify.

defineComponent

The first generic parameter of defineComponent is the Props type.

<script lang="ts">
import ComponentA from 'componentA.vue'

interface AdditionalProps {
  // ... define your props here
}

type Props = AdditionalProps & (typeof ComponentA)['$props']

const ComponentB = defineComponent<Props>({
  props: {
    ...(ComponentA.props as any) //This line ensures that "additional" props are not handled as "attrs"
  }
})
</script>

<template>
  <ComponentA v-bind="props">
</template>

NOTE ON SomeComponent[$props]

Although not publicly documented, editor tools rely on this for auto-complete functionalities.

To extract any component props, a utility type can be defined as follows:

type ComponentProps<T extends {$props: any}> = Omit<T['$props'], `$${string}` | `v-slot${string}`>

The reason for using Omit on specified string patterns is to prevent Typescript from becoming slow during editing or error reporting due to internal properties.

Extending/Typing slots

In addition, you can leverage defineSlots from script setup, introduced in Vue 3.3, or employ a workaround similar to what was used in older versions of vue-router.

An example of using defineSlots taken from the blog post

<script setup lang="ts">
defineSlots<{
  default?: (props: { msg: string }) => any
  item?: (props: { id: number }) => any
}>()
</script>

Using defineComponent (credit goes to Abdelrahman Awad's Blog post)

<script lang="ts">
import ComponentA from 'componentA.vue'
    
const ComponentB = defineComponent({
  //... component definition
})
    
const AugmentedComponentB = ComponentB as typeof ComponentB & {
  new(): {
     $slots: (typeof ComponentA)['$slots']
   }
}
export default AugmentedComponentB
</script>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best way to forward all URLs to one central page?

I've recently started working with Angular and I'm currently developing a web app project using Angular 9. I could really use your help with this. My goal is to have a dynamic URL structure for the web app, such as https://www.myMainURL.com/***, ...

How can JavaScript transform Unicode strings?

<i class="icon">&#xe672;</i> This code will display an icon like this: > However, when I render it in Vue: <i class="icon">{{a}}</i> a = '&#xe672;' The result is  It displays as a string! ...

Encountered an issue in React Native/Typescript where the module 'react-native' does not export the member 'Pressable'.ts(2305)

I have been struggling to get rid of this persistent error message and I'm not sure where it originates from. Pressable is functioning correctly, but for some reason, there is something in my code that doesn't recognize that. How can I identify t ...

I'm experiencing difficulties with the "AXIOS PATCH" functionality

I am attempting to modify a database record using the "axios patch" method. This is the code I have tried: editClient(client) { let data = new FormData(); data.append("name", this.client.name); data.append("email", this.client.email); data.append ...

Help me figure out how to independently toggle submenus on my navbar by utilizing the Vue.js composition API

I am currently working on developing a navbar using the Vue.js composition API. My primary objective is to toggle the submenus when a user clicks on them. However, since I am iterating through an object to display the submenus, I am faced with the challeng ...

Exploring the Benefits of Combining Vue.js with Laravel

Being a newcomer to Vue, I decided to try it out in a recent project and quickly understood why it's so popular. Everything was running smoothly until I tested it in IE, where nothing seemed to work at all. Encountering errors like Object doesn' ...

Updating the value of a key in an object is not functioning as expected

There is a single object defined as requestObject: any = { "type": 'type1', "start": 0, "size": 10, "keywords": ['abcd','efgh'], filters: [], } Next, attempting to change the value for keyword, I updat ...

Convert the date into a string format instead of a UTC string representation

I am currently working on a node.js project using TypeScript. In this project, I have a Slot class defined as follows: export class Slot { startTime: Date; constructor(_startTime: Date){ this.startTime = _startTime } } // Within a controller method ...

Eliminate text from template literals in TypeScript types

I have successfully created a function that eliminates the 'Bar' at the end of a string when using foo. However, is there a way to achieve the same result using the type statement? (refer to the code snippet below) declare function foo<T exten ...

Generate a dropdown menu with dynamic options populated from an API by adding an input type select element dynamically

Greetings! I am working on designing a decision tree that dynamically generates options based on user selections and API responses. When a user chooses a reason option, the corresponding reasons are fetched from the API and displayed in a select dropdown. ...

"Default slot encountered a non-functional value" within a Vue 3 Composition API component

MCVE https://github.com/hyperbotauthor/minvue3cliapp MCVE live https://codesandbox.io/s/white-browser-fl7ji In my Vue 3 cli-service application, I have implemented composition API components with slots. The HelloWorld component renders the slots passe ...

Transferring client-side data through server functions in Next.js version 14

I am working on a sample Next.js application that includes a form for submitting usernames to the server using server actions. In addition to the username, I also need to send the timestamp of the form submission. To achieve this, I set up a hidden input f ...

In order to use Vue3 with Vite, it is necessary to use kebab case tags for custom components, whereas Vue3 CLI allows the

I am working on a project using Vue3 with Vite (on Laravel) that has a Wiki.vue page which loads a "MyContent.vue" component. //On MyContent.vue: <template> <div>content component</div> </template> <script> export default ...

The form action seems to be unresponsive when utilized within a vue-bootstrap form

I'm utilizing a form submission service called formsubmit.co, which allows forms to receive input data via email without the need to develop a backend for storing and transmitting data. Formsubmit handles all the storage and sending processes. Accordi ...

Implementing Vuejs sorting feature post rendering

Currently, I have elements linked to @click event listeners. <th @click="sort('dateadded')" class="created_at">Date Added I am looking for a way to automatically trigger this sorting function when the Vue.js component renders, so that th ...

Unable to locate the <router-view> component within VueRouter

I recently completed a project using Vue.js 3.2.13 and Vue-Router 4.0.14. Despite my efforts to ensure everything was set up correctly, I encountered an error in the browser that said "[Vue warn]: Failed to resolve component: router-view". The specific lin ...

The peculiar behavior of Google Maps markers refreshing inconsistently upon bounds adjustment

I recently completed a project involving a restaurant app using Vue and Google Maps. While everything is functional, I have encountered a persistent bug with the markers. When navigating on the map and the bounds change, some markers seem to disappear. Ev ...

Create a loop in Vue.js 3 without the need for a query

Can someone help me understand how to fetch data using a loop in Vue.js version 3 when working with queries? I am trying to retrieve an object based on its id, which is obtained from the URL. However, I seem to be facing some difficulties. Any guidance wou ...

A bespoke Typescript implementation of nested lists containing numbers

Currently, I am trying to figure out how to declare and populate a TypeScript list of lists. The structure of the list should be as follows: List<CustomList<>, number> Typically, I would create a standard list like this: someList: { text: a ...

Generating interactive elements in VUE is key

I am unsure about how to dynamically create components without using the <component :is=''> tag. I would like to insert a component into the DOM through JavaScript. Similar to how you would add a new modal in jQuery with $("body").append(" ...