3

I was asking this question with a more complex version of this basic concept
Rel: Can Generic JSX.Elements work in Typescript

I narrowed it down to the core Elements:

This is Object A that takes parameters from TypeA

type TypeA = {
  label: string
  value: number
}
const ObjA = ({ label, value }:TypeA) => {
  return <div>
    <div>Label: {label}</div>
    <div>Value: {value}</div>
  </div>
}

This is Object B that takes parameters from TypeB

type TypeB = {
  label: string
  value: string
  bool: boolean
}
const ObjB = ({ label, value, bool }:TypeB) => {
  return <div>
    <div>Label: {label}</div>
    {bool && <div>Value: {value}</div>}
  </div>
}

Now I collect this ComponentGroup inside an array and create a Type out of this Array:

const ComponentCollection = [
  ObjA,
  ObjB
] as const
type Components = typeof ComponentCollection[number]

Then I create a generic component:

interface GenericProps<T extends Components> {
  Component: T
  title: string
}
const Generic = <T extends Components,>({ Component, title, ...props }:GenericProps<T>) => {
  return (
    <div>
      <label>{title}</label>
      <Component {...props}/>
  </div>
  )
}

At last I can call the generic component as follows:

<Generic Component={ObjA} title={'Usage A'}           label={'Object A'} value={'String A'}/>
<Generic Component={ObjB} title={'Usage B no Bool'}   label={'Object B'} value={0}/>
<Generic Component={ObjB} title={'Usage B with Bool'} label={'Object B'} value={0} bool/>

Altough it works really well in JavaScript, I messed something up with the typing.

I setup one TS-Playground and one Codepen:
TS-Playground: https://tsplay.dev/WvVarW
Codepen: https://codepen.io/Cascade8/pen/eYezGVV

Goal:

  1. Convert this code above in correct TypeScript code
  2. Compile without any TS-Errors or /@ts-ignore
  3. Make IntelliSense work, so if you type <Generic Component={ObjA} ... it shows the available type attributes for this Object. In this case: label={string: } value={string: }

What i don't want:

  1. Usage of classes or the old function syntax as our EsLint requires us to use an Arrow-Function if possible.
  2. Passing the Objects as a Child.
    I know this works, but it is not the prefered solution as the main Project has a lot of groups like this that get rendered like this.
    And why shouldn't something work in TypeScript that works very simple in JavaScript.
Cascade_Ho
  • 61
  • 5

2 Answers2

3

Following the way you've constructed your types, you can do it using the definition of your generic inside GenericProps

(Note, I have bundled the props into a new props prop, as this should avoid name collisions should you have naming collisions)

import React from 'React'

type TypeA = {
  label: string
  value: number
}
const ObjA = ({ label, value }:TypeA) => {
  return <div>
    <label>{label}</label>
    <label>{value}</label>
  </div>
}
type TypeB = {
  label: string
  value: string
  bool: boolean
}
const ObjB = ({ label, value, bool }:TypeB) => {
  return <div>
    <label>{label}</label>
    {bool && <label>{value}</label>}
  </div>
}

type Components = typeof ObjA | typeof ObjB;

interface GenericProps<T extends (...args: any) => any> {
  Component: T
  title: string
  props: Parameters<T>[0]
}

const Generic = <T extends Components,>({ Component, title, props }:GenericProps<T>) => {
  return (
    <div>
      <label>{title}</label>
      <Component {...props as any}/>
    </div>
  )
}
const Usage = () => {
  return <Generic Component={ObjA} title={'Usage'} props={{label: 'ObjectA'}}/>
}
export default Generic
Tom
  • 1,158
  • 6
  • 19
  • Thank you, this is the closest thing to achieve it. I recreated it in TS Playground: https://tsplay.dev/WyblKw Sadly there still is an Error inside the Component part – Cascade_Ho Feb 03 '22 at 11:08
  • Yes, you're right. It's because inside `Generic` the type of props isn't specified as matching the entry in `Components`, that's done inside `GenericProps`. You could assert the type in the line that you use them though. You could just cast to `as any` inside there. It doesn't affect the types inside `Generic` – Tom Feb 03 '22 at 11:10
  • @Cascade_Ho, call Component as a normal function rather than via JSX, i.e., `Component(props)` rather than ``. That will avoid the LibraryManagedAttributes that are causing your issue, I believe. – sam256 Feb 04 '22 at 14:35
1

It is possible to do without any kind of type assertions. Consider this exmaple:

import React, { FC, } from 'react'

type Type = {
  label: string;
  value: string | number
}

type TypeA = {
  label: string
  value: number
}

type FixSubtyping<T> = Omit<T, 'title'>

const ObjA = ({ label, value }: FixSubtyping<TypeA>) => {
  return <div>
    <label>{label}</label>
    <label>{value}</label>
  </div>
}

type TypeB = {
  label: string
  value: string
  bool: boolean
}

const ObjB = ({ label, value, bool }: FixSubtyping<TypeB>) => {
  return <div>
    <label>{label}</label>
    {bool && <label>{value}</label>}
  </div>
}


type Props<T> = T & {
  title: string
}


const withTitle = <T extends Type>(Component: FC<FixSubtyping<Props<T>>>) =>
  ({ title, ...props }: Props<T>) => (
    <div>
      <label>{title}</label>
      <Component {...props} />
    </div>
  )

const GenericA = withTitle(ObjA)
const GenericB = withTitle(ObjB)

const jsxA = <GenericA title={'Usage'} label={'A'} value={42} /> // // ok

const jsxB = <GenericB title={'Usage'} label={'A'} value={'str'} bool={true} /> // // ok

Playground

This error occurs because props is infered as Omit<T,'title'> so we should assure TS that Component props is compatible with Omit<T,'title'>

However there is a drawback, you need to update props type in other components. If it is not an option, I think the best approach would be to overload your Generic function:

import React, { FC, } from 'react'

type TypeA = {
  label: string
  value: number
}

type FixSubtyping<T> = Omit<T, 'title'>

const ObjA = ({ label, value }: TypeA) => {
  return <div>
    <label>{label}</label>
    <label>{value}</label>
  </div>
}

type TypeB = {
  label: string
  value: string
  bool: boolean
}

const ObjB = ({ label, value, bool }: TypeB) => {
  return <div>
    <label>{label}</label>
    {bool && <label>{value}</label>}
  </div>
}


type Props<T> = T & {
  Component: FC<T>,
  title: string
}


function Generic<T,>(props: Props<T>): JSX.Element
function Generic({ Component, title, ...props }: Props<unknown>) {
  return (
    <div>
      <label>{title}</label>
      <Component {...props} />
    </div>
  )
}

const jsxA = <Generic Component={ObjA} title={'Usage'} label={'A'} value={42} /> // // ok

const jsxB = <Generic Component={ObjB} title={'Usage'} label={'A'} value={'str'} bool={true} /> // // ok

Playground