-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Is there any way to use generic when defining props? #3102
Comments
Vue handles props differently from react, in vue a prop can have runtime validation. I have this #3049 PR to introduce a similar way to pass the type to props, but this will still require you to define the props object. I might misunderstand your issue, please clarify |
I know vue can do a runtime validation. What I need is to make different prop got connected by generic. For example you can create a select component with props:
But if I do this there would be a lot of problem when handling prop internally and do prop type check. Conceptually the best way is to specify the prop like this
React component libraries do a lot like this. |
Is there any other place I can follow discussions/progress about this one? |
If you only need generic props then you can use this tutorial: If anyone has solution to create generic component with both generic props and generic slots, please, share your ideas. |
I've a hacky workaround (with setup only), it works for tsx, ts, template. However I don't recommend it. I think it isn't a good idea to implement a generic component before vue officially support it. import { h, OptionHTMLAttributes, SelectHTMLAttributes, VNodeChild } from 'vue'
/**
* tsconfig:
*
* "jsx": "react",
* "jsxFactory": "h",
* "jsxFragmentFactory": "Fragment",
*
*/
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
interface ElementChildrenAttribute {
$slots: {}
}
interface IntrinsicElements {
select: { $slots: any } & SelectHTMLAttributes // 不加这个的话 vue 内置 jsx 元素没有 $slots
option: { $slots: any } & OptionHTMLAttributes // 不加这个的话 vue 内置 jsx 元素没有 $slots
}
}
}
interface SelectProps<T extends string | number> {
value?: T
options?: Array<{ label: string, value: T }>
}
interface SelectSlots<T extends string | number> {
option?: (option: { label: string, value: T }) => VNodeChild
}
// 关键步骤在这里
const _Select = class <T extends string | number = string | number> {
$props: SelectProps<T> & { $slots?: SelectSlots<T> } = null as any
$slots?: SelectSlots<T>
constructor () {
return this as any
}
setup (
props: SelectProps<T>,
{ slots }: { slots: SelectSlots<T> }
): () => VNodeChild {
return () => {
return (
<select value={props.value}>
{props.options?.map((option) => {
return slots.option ? (
slots.option(option)
) : (
<option value={option.value}>{option.label}</option>
)
})}
</select>
)
}
}
}
function resolveRealComponent<T> (fakeComponent: T): T {
return {
setup: (fakeComponent as any).prototype.setup
} as any
}
const TestSelect = resolveRealComponent(_Select)
const vnode1 = h(TestSelect, {
value: '123',
options: [{ label: '1243', value: 123 }]
})
const vnode2 = (
<TestSelect value={123} options={[{ label: '123', value: 134 }]}>
{{
option: ({ label, value }) => {
return 1
}
}}
</TestSelect>
)
console.log(vnode1, vnode2)
export { TestSelect }
// Select<Option, Clearable, LabelField, ValueField>
// Cascader<Option, Clearable, LabelField, ValueField, ChildrenField> |
@07akioni maybe we can use class component + tsx. and it can solve all the pain points. see https://agileago.github.io/vue3-oop/ exampleimport { type ComponentProps, Mut, VueComponent } from 'vue3-oop'
import type { VNodeChild } from 'vue'
interface GenericCompProp<T> {
data: T[]
slots?: {
itemRender(item: T): VNodeChild
}
}
class GenericComp<T> extends VueComponent<GenericCompProp<T>> {
static defaultProps: ComponentProps<GenericCompProp<any>> = ['data']
render() {
const { props, context } = this
return (
<>
<h2>GenericComp</h2>
<ul>{props.data.map(k => context.slots.itemRender?.(k))}</ul>
</>
)
}
}
export default class HomeView extends VueComponent {
@Mut() data = [1, 2]
render() {
return (
<div>
<h1>home</h1>
<GenericComp
data={this.data}
v-slots={{
itemRender(item) {
return <li>{item}</li>
},
}}
></GenericComp>
</div>
)
}
} |
We need to use classes to solve this, classes are not planned to be supported. Another way to do this (hacky overhead), would be doing something like: import { Component, defineComponent } from 'vue'
function genericFunction<G extends { new(): { $props: P } }, P, T extends Component>(f: () => T, c: G): G & T {
return f() as any
}
declare class TTTGenericProps<T extends { a: boolean }> {
$props: {
item: T,
items?: T[]
}
}
const TTT = genericFunction(<T extends { a: boolean }>() => defineComponent({
props: {
item: Object as () => T,
items: Array as () => T[],
},
emits: {
update: (a: T) => true
},
setup(props, { emit }) {
// NOTE this should work without casting
props.items?.push(props.item! as T)
// @ts-expect-error not valid T
props.items?.push(1)
props.items?.push({ a: false } as T)
// @ts-expect-error
props.items?.push({ b: false } as T)
emit('update', props.items![0])
// @ts-expect-error
emit('update', true)
}
// casting undefined to prevent any runtime cost
}), undefined as any as typeof TTTGenericProps);
; <TTT item={{ a: true, b: '1' }} items={[{ a: false, b: 22 }]} />
// @ts-expect-error
; <TTT item={{ aa: true, b: '1' }} items={[{ a: false, b: 22 }]} /> |
For anyone interested I found the following solution: I use GlobalComponentConstructor type from Quasar framework: // Quasar type
type GlobalComponentConstructor<Props = {}, Slots = {}> = {
new (): {
$props: PublicProps & Props
$slots: Slots
}
}
interface MyComponentProps<T> {
// Define props here
}
interface MyComponentSlots<T> {
// Define slots here
}
type MyComponentGeneric<T> = GlobalComponentConstructor<MyComponentProps<T>, MyComponentSlots<T>>;
defineComponent({
name: "another-component",
components: {
"my-component-generic-boolean": MyComponent as unknown as MyComponentGeneric<boolean>,
"my-component-generic-string": MyComponent as unknown as MyComponentGeneric<string>
}
} Volar and vue-tsc recognize the above pattern. The downside is that I need to define Slots and Props interfaces. It would be ideal if Vue added defineComponent<Slots, Props> version of defineComponent that would validate my Slots and Props interfaces. |
I will add an example of my ideal usage pattern, and explain what I am currently doing to work around this. +1. types.d.ts export interface Image {
filename?: string;
src: string;
}
export interface PhotoImage extends Image {
fValue?: number;
shutterSpeed?: number;
iso?: number;
} ImageGallery.vue <template>
<div class="image-gallery">
<!-- ..stuff.. (display images list) -->
<section v-if="$scopedSlots['selected-image-viewer']">
<slot name="selected-image-viewer" :selected-image="selectedImage"></slot>
</section>
</div>
</template>
<script lang="ts">
import { Image, PhotoImage } from "@/types";
import { defineComponent, PropType, Ref, ref } from "@vue/composition-api";
export default defineComponent({
props: {
images: {
// currently, this is not possible? And so I just use `PropType<Image>` instead
type: PropType<T extends Image>,
default: [],
},
setup(props) {
const selectedImage: Ref<null | T> = ref(null); // currently `Ref<null | Image>`
// ... logic that sets a "selected" image to one of the images on click
return {
selectedImage,
};
},
});
</script> PhotosPage.vue <template>
<div id="my-page">
<ImageGallery :images="images">
<template #selected-image-viewer="{ selectedImage }">
<!-- So here, selectedImage should be of type `PhotoImage`, because we gave `PhotoImage[]`. But,
right now it is type Image, and doesn't have the fields of PhotoImage. As a workaround I cast
to `any` within the component and expose that for the slot instead (no type safety).
-->
</template>
</ImageGallery>
</div>
</template>
<script lang="ts">
import { PhotoImage } from "@/types";
import { defineComponent, Ref, ref } from "@vue/composition-api";
import ImageGallery from "@/components/ImageGallery.vue";
export default defineComponent({
components: {
ImageGallery,
},
setup() {
const images: Ref<PhotoImage[]> = ref([]);
// set images somehow
return {
images,
}
},
});
</script> |
The solutions above seems all not work with I found out that now volar can support generics for const Component = <T>(props: Props<T>, context: SetupContext) => {
return h('div');
} Now, use this component can get generic props work, but will lost the scoped slots type. My idea is to extend the functional component formats like this: add slots to const Component = <T, P = Props<T>, S = Slots<T>, E = Events<T>>(props: P, context: SetupContext<E, S>) => {
return ('div'),
} |
I was able to get volar to properly see props/slots, but it required manually defining the The <template>
<div>
<template v-if="isLoading">
<slot name="loading">Loading</slot>
</template>
<template v-else-if="isError">
<slot name="error" v-bind="{ error }">
{{ error }}
</slot>
</template>
<template v-else>
<slot name="success" v-bind="{ data }">
<pre>{{ JSON.stringify(data, null, 2) }}</pre>
</slot>
</template>
</div>
</template>
<script setup lang="ts">
import type { VNode } from 'vue-demi'
export interface Props<D, E> {
query: {
isLoading: Ref<boolean>
isError: Ref<boolean>
error: Ref<E>
data: Ref<D>
}
}
export interface Slots<D, E> {
loading?: () => Array<VNode> | undefined
error?: (context: { error: E }) => Array<VNode> | undefined
success?: (context: { data: D }) => Array<VNode> | undefined
}
const props = defineProps<Props<unknown, unknown>>()
const { isLoading, isError, error, data } = props.query
</script> And then the <script lang="ts">
import NoGenerics from './NoGenerics.vue'
import type { Props, Slots } from './NoGenerics.vue'
type WithGenerics = new <D, E>(props: Props<D, E>) => {
$props: Props<D, E>
// Use `$slots` for vue 3
$scopedSlots: Slots<D, E>
}
export default NoGenerics as WithGenerics
</script> |
Thank you @achaphiv. I used your approach to make my component handle generic properties and it worked perfectly with Volar 0.40.13. Unfortunately, once I upgraded to Volar 1.0.0, the Vue props no longer inferred their types from the values assigned. When I hover over the prop it now shows I've had to downgrade Volar back to 0.40.13 for it to work properly in VS Code. I should note that in Volar 1.0.0, the code still compiles with |
I just want to mention this video which outlines (and solves for React) this exact issue - https://www.youtube.com/watch?v=hBk4nV7q6-w |
BREAKING CHANGE: The type of `defineComponent()` when passing in a function has changed. This overload signature is rarely used in practice and the breakage will be minimal, so repurposing it to something more useful should be worth it. close vuejs#3102
What problem does this feature solve?
Robust prop definition. See the following pic.
What does the proposed API look like?
I have not come up with it.
However in react it does work.
https://codesandbox.io/s/epic-knuth-lffi0?file=/src/App.tsx:0-785
I tried functional component, it doesn't work either.
The text was updated successfully, but these errors were encountered: