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.
0

2 Answers 2

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
Sign up to request clarification or add additional context in comments.

3 Comments

Thank you, this is the closest thing to achieve it. I recreated it in TS Playground: tsplay.dev/WyblKw Sadly there still is an Error inside the Component part
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
@Cascade_Ho, call Component as a normal function rather than via JSX, i.e., Component(props) rather than <Component {...props}/>. That will avoid the LibraryManagedAttributes that are causing your issue, I believe.
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

7 Comments

Ty for your Solution. Is this Possible to do this with an Arrow Function ? I somehow can't get it to work
@Cascade_Ho unfortunately no, it should infer T generic parameter
Okay. Well, EsLint ignore > @ts-ignore, I guess :D
I just tried to adapt this to my main Problem, but now i have no type restrictions for the Component. Is it possible to restrict that in the bottom Solution ?
@Cascade_Ho could you please provide me with example, I mean where exact you have a problem with type restrictions?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.