How can I make a Vue component with a generic type parameter that extends the properties of the component? #15178
|
I'm working in creating a Vue component that can mount components from other frameworks. Don't worry about the mechanics of this action, as they are all complete and working. What I would love is for Intellisense to be able to show the properties of the mounted component as if they were properties of the container component itself. The main problem is type Props = TProps & {
/* own properties */
};
const props = defineProps<Props>();Is there a way to keep defineProps() happy while still extending the exposed properties type of the container component? |
Replies: 2 comments 2 replies
|
You've hit a real limit, not a syntax mistake. The way through is <script setup lang="ts" generic="TProps extends Record<string, any>">
const props = defineProps</* @vue-ignore */ TProps & {
component: unknown
}>()
</script>That tells the compiler to skip runtime prop generation for that type and keeps the type surface for TS, which is the part you actually want for Intellisense. The tradeoff: only your own props get a runtime declaration. I compiled the above with props: {
component: { type: null, required: true }
}No That's not a bug you can configure away, by the way. Vue has to emit the props list when it compiles your component, and |
|
The accepted answer is technically correct about Why your problem happens: When a Vue component receives a prop from a template via kebab-case ( The cleanest fix that solves case-sensitivity + Intellisense + runtime: split the prop type into a static set (your own) and a dynamic set (wrapped component's), and bind via template attribute syntax that preserves casing: <!-- Wrapper.ce.vue -->
<script setup lang="ts" generic="TWrapped">
// Static prop: declared normally so it's known at compile time
defineProps<{ component: string }>()
// Dynamic props forwarded to the wrapped component as JS properties
const instance = getCurrentInstance()!
</script>
<template>
<!-- bind the wrapped component's runtime props by reading from attrs
and forwarding them as camelCase JS properties, NOT as HTML attrs -->
<component
:is="instance.props.component"
v-bind="filterAttrs($attrs)"
/>
</template>// utils.ts
export function filterAttrs(attrs: Record<string, unknown>) {
// camelCase keys are already in the right shape; only drop Vue-internal ones
const out: Record<string, unknown> = {}
for (const k in attrs) {
if (!k.startsWith('on') && k !== 'class' && k !== 'style') {
out[k] = attrs[k]
}
}
return out
}Now in the host: <MyWrapper
:component="SomeReactLib"
clearOnDispatch
:user-id="42"
/>Both If you specifically need HTML-attribute–style binding across the boundary (e.g. server-rendered HTML with attributes),
const WrapperEl = defineCustomElement(Wrapper, { shadowRoot: false })
customElements.define('vue-wrapper', WrapperEl)
// host side — no case conversion needed:
document.querySelector('vue-wrapper')!.clearOnDispatch = trueSo the case-sensitivity issue is solved by: (1) reading from TL;DR: |
You've hit a real limit, not a syntax mistake.
definePropsresolves its type at compile time, and it can't resolve a type parameter, soTProps & { ... }fails with "Unresolvable type reference". Addinggeneric="TProps"on its own doesn't help.The way through is
@vue-ignore:That tells the compiler to skip runtime prop generation for that type and keeps the type surface for TS, which is the part you actually want for Intellisense.
The tradeoff: only your own props get a runtime declaration. I compiled the above with
@vue/compiler-sfc