Skip to content

Is there any way to use generic when defining props? #3102

@07akioni

Description

@07akioni
Contributor

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

image

I tried functional component, it doesn't work either.

image

Activity

pikax

pikax commented on Feb 1, 2021

@pikax
Member

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

07akioni

07akioni commented on Feb 1, 2021

@07akioni
ContributorAuthor

#3049

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:

{
  value: string | string[]
  onChange: (value: string) => void | (value: string[]) => void
}

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

<T extends string | string[]>{
  value: T
  onChange: (value: T) => void
}

React component libraries do a lot like this.

oswaldofreitas

oswaldofreitas commented on Oct 11, 2021

@oswaldofreitas

Is there any other place I can follow discussions/progress about this one?

iliubinskii

iliubinskii commented on Feb 3, 2022

@iliubinskii

If you only need generic props then you can use this tutorial:
https://logaretm.com/blog/generically-typed-vue-components/
It worked for me
BUT:
It does not help in creating generic slots.

If anyone has solution to create generic component with both generic props and generic slots, please, share your ideas.

07akioni

07akioni commented on Feb 3, 2022

@07akioni
ContributorAuthor

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>
agileago

agileago commented on Feb 4, 2022

@agileago

@07akioni maybe we can use class component + tsx. and it can solve all the pain points. see https://agileago.github.io/vue3-oop/

example

import { 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>
    )
  }
}

image

pikax

pikax commented on Feb 17, 2022

@pikax
Member

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 }]} />

playground

iliubinskii

iliubinskii commented on Feb 17, 2022

@iliubinskii

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.
As a result I get type safety both for props and slots.

The downside is that I need to define Slots and Props interfaces.
However, Quasar does the same.

It would be ideal if Vue added defineComponent<Slots, Props> version of defineComponent that would validate my Slots and Props interfaces.

14 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      Participants

      @colinj@achaphiv@ccqgithub@pikax@oswaldofreitas

      Issue actions

        Is there any way to use generic when defining props? · Issue #3102 · vuejs/core