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

Is there a way to access the rootPath or other client-side information from the language server side?

I'm currently developing a language extension based on the example "language server" available at https://code.visualstudio.com/docs/extensions/example-language-server. In order to determine the current folder being used by vscode on the server side, ...

The program encountered an error because it was unable to access the overview property since it was undefined

I encountered the error below Cannot read property 'overview' of undefined I am struggling to understand what is causing this issue. <template> <tr v-for="(row, fold) of perFold" :key="fold"> <td>{{ ...

Issue with Vue Loading Overlay Component functionality in nuxt2 .0

I've integrated the vue-loading-overlay plugin into my project. plugins/vueloadingoverlaylibrary.js import Vue from 'vue'; import Loading from 'vue-loading-overlay'; // import 'vue-loading-overlay/dist/vue-loading.css'; ...

Error in Next.js when trying to use Firebase Cloud Messaging: ReferenceError - navigator is not defined in the Component.WindowMessagingFactory instanceFactory

Currently, I am in the process of setting up push notifications with Firebase in my next.js application. I have been following a guide from the documentation which you can find here: https://firebase.google.com/docs/cloud-messaging/js/receive?hl=es-419 Ho ...

issue with leaflet vue3 component showing incorrect marker popup when clicked

I am dynamically loading markers based on the bounding box from an API using a Pinia store. These markers are stored in the itemsList property of my map wrapper component map-view. The markers are rendered as circlemarkers inside a Vue-for loop. As I navi ...

Guide to integrating a menu in Vue.js

I am currently facing an issue with importing a component from vue.js into my App.vue file. Here is the code for the component: <template> <h1> Hello</h1> </template> <script> export default { } </script> And here ...

Passing data between child components using Vuejs 3.2 for seamless communication within the application

In my chess application, I have a total of 3 components: 1 parent component and 2 child components. The first child component, called Board, is responsible for updating the move and FEN (chess notation). const emit = defineEmits(['fen', 'm ...

Are 'const' and 'let' interchangeable in Typescript?

Exploring AngularJS 2 and Typescript led me to create something using these technologies as a way to grasp the basics of Typescript. Through various sources, I delved into modules, Typescript concepts, with one particularly interesting topic discussing the ...

The `Home` object does not have the property `age` in React/TypeScript

Hey there, I'm new to React and TypeScript. Currently, I'm working on creating a React component using the SPFX framework. Interestingly, I'm encountering an error with this.age, but when I use props.age everything seems to work fine. A Typ ...

Creating a Robust Next.js Build with Tailor-Made Server (Nest.js)

I'm in need of assistance with compiling/building my project using Next.js while utilizing a custom server. Currently, I have integrated Nest.js (a TypeScript Node.js Framework) as the backend and nested Next.js within it. The integration seems to be ...

Encountering an error when trying to access this.$store.state in a basic Vuex

Encountered an error with return this.$store.state.counter: The property '$store' does not exist on the type 'CreateComponentPublicInstance<{}, {}, {}, { counter(): any; }, {}, ComponentOptionsMixin, ComponentOptionsMixin, EmitsOptions, ...

What is the best way to access the original observed node using MutationObserver when the subtree option is set to

Is there a way to access the original target node when using MutationObserver with options set to childList: true and subtree: true? According to the documentation on MDN, the target node changes to the mutated node during callbacks, but I want to always ...

Struggling to use the bind method for the loadScene callback function in cocosCreator or cocos2d-x?

When the loadScene() callback function is bound, the information retrieved from getScene() becomes inaccurate. Upon transitioning from the Entry Scene to the Lobby Scene, I perform post-processing tasks. The implementation was done in TypeScript. Entry. ...

Using the vue-router useRouter within library components does not function as expected during the building process

I am currently developing a Vue3 npm component library, and my goal is to be able to access the current router using vue-router's useRouter function. I want this functionality to be automatically provided by any Vue application importing my library co ...

Vue cookies experiencing issues with updating cookie values correctly

My goal is to store user preferences (maximum 2-3 users) in a cookie for easy access. Upon login, I first check if the 'users' cookie exists. If not, I create it. If it does exist, I check if the current user is included in the cookie. If not, I ...

Issue with Ionic Native File: File.writeFile function - file is not being created and there is no callback response

I've exhausted all the different solutions I could find, but unfortunately, the file isn't getting saved and nothing seems to be happening. The callback functions aren't being called - neither success nor error. Here are the solutions I&apo ...

Tips for including a decimal point in an angular reactive form control when the initial value is 1 or higher

I am trying to input a decimal number with 1 and one zero like 1.0 <input type="number" formControlName="global_velocity_weight" /> this.form = this.fb.group({ global_velocity_weight: new FormControl(1.0, { validators: [Valida ...

Tips on ensuring the <script> section of a Vuejs single file component loads prior to the <template> section

After posing my query here, I have come to the realization that the root of my problem might be due to the template loading before the script. This causes an error when it encounters an undefined variable, halting the script execution. The issue could pote ...

Guide to displaying the value of a field in an object upon clicking the inline edit button using TypeScript

Is it possible to console log a specific attribute of an object when clicking on the edit button using the code below? Please provide guidance on how to utilize the index to access the value of "name". Refer to the last line in the code with the comment. ...

Exploring the Power of Vue 3 in Manipulating the DOM

Hello everyone, I am a beginner with Vue and I am interested in learning how to modify the DOM without relying on methods such as document.querySelector or getElementById. Let's take for instance this input: <input id="myInputId" class=& ...