3

I am quite new to Typescript generics. Right now, I have to merge different Components from old React-Projects into a Typescript Component library. One of those old Components are SurveyComponents, containing things like Dropdown etc.

What i did before:

Previously we could dynamically render them with the following Code:

const [optionValue, setOptionValue] = useState<Option<string>>({ value:'', id:'' })
const [textValue,   setTextValue]   = useState<Option<string>>({ value:'', id:'' })
const [ratingValue, setRatingValue] = useState<Option<number>>({ value: 0, id:'' })
const [arrValues,   setArrValues]   = useState<Option<string>[]>([])

<Question Component={Radio}    value={optionValue} setValue={setOptionValue} options={options} maxColumns={3}  title={'Radio:'} />
<Question Component={Checkbox} value={arrValues}   setValue={setArrValues}   options={options} maxColumns={3}  title={'Checkbox:'} />
<Question Component={Dropdown} value={optionValue} setValue={setOptionValue} options={options} Icon={WarnIcon} title={'Dropdown:'} />
<Question Component={Rating}   value={ratingValue} setValue={setRatingValue} Icon={WarnIcon}   max={3}         title={'Rating:'}  />
<Question Component={Text}     value={textValue}   setValue={setTextValue}                                     title={'Textfield:'}/>
<Question Component={Text}     value={textValue}   setValue={setTextValue}   area                              title={'Textarea:'} />

Inside of the Questioncomponent it was used as follows:

const Question = ({ Component, description, title, ...props }:QuestionProps) => {
  return (
    <div className={'question-wrapper'}>
      <div className={'question-title'}>{title}</div>
      <div className={'question-description'}>{description}</div>
      <div className={'question-component'}>
        <Component {...props}/>
      </div>
    </div>
  )
}
export default Question

This works completely fine, so it should work in Typescript aswell?

One of many approaches in Typescript:

export const SurveyComponents = [
  Radio,
  Checkbox,
  Text,
  Dropdown,
  Rating
] as const

export type SurveyComponentTypes = typeof SurveyComponents[number]

export interface QuestionProps<T> {
  Component: T
  title: string
  description?: string
}

const Question = <T extends SurveyComponentTypes>({ Component, description, title, ...props }:QuestionProps<T>) => {
  return (
    <div className={'question-wrapper'}>
      <div className={'question-title'}>{title}</div>
      <div className={'question-description'}>{description}</div>
      <div className={'question-component'}>
        <Component {...props}/>
      </div>
    </div>
  )
}
export default Question

This sadly gives me the following Error:

TS2322: Type '{}' is not assignable to type 'LibraryManagedAttributes '.

If i remove the <T extends SurveyComponentTypes> and just use <T,>

it returns this Error:

TS2604: JSX element type 'Component' does not have any construct or call signatures

In Question, when calling the Components above, it gives this error (for Dropdown):

TS2322: 
Type '{ Component: ({ options, value, setValue, placeholder, children, Icon, allowEmpty }: DropDownProps) => Element;
 value: Option<string>; setValue: Dispatch<SetStateAction<Option<string>>>; options: Option<...>[]; Icon: ({ className, onClick, size }: IconProps) => Element;
 title: string; }' 
is not assignable to type 
'IntrinsicAttributes & QuestionProps<({ options, value, setValue, placeholder, children, Icon, allowEmpty }: DropDownProps) => Element>'.   
Property 'value' does not exist on type 'IntrinsicAttributes & QuestionProps<({ options, value, setValue, placeholder, children, Icon, allowEmpty }: DropDownProps) => Element>

Expected Behaviour

  1. Compile without any Typescript errors
  2. Get IntelliSense to work so if you type <Question Component={Dropdown} ...>it shows the available props for Dropdown.

I spent ages finding the correct Syntax, but I can't find anything. I need a solution for ArrowFunctions as our EsLint forces us to use ArrowFunctions if possible.

If there is really no way to implement this, we might have to use classes again (Not prefered)

EDIT:

Yes!, I know I could just pass it as a child, but this is not the desired outcome, since this solution above is fine in JavaScript, but the TypeScript Syntax is the thing I don't know.

Cascade_Ho
  • 61
  • 5
  • what was the objective of this line? `export type SurveyComponentTypes = typeof SurveyComponents[number]` – davidgamero Feb 02 '22 at 16:38
  • You _can_ do this with overloads, but since you're not doing anything with the component besides rendering it as a child, why not just render it in the parent and pass the element as a child? – jsejcksn Feb 02 '22 at 16:53
  • @davidgamero You can create a type based on an readonly Array – Cascade_Ho Feb 02 '22 at 21:56
  • Try calling `Component` using normal function signature rather than JSX, i.e., inside Question, just call it as `Component({...props})`. That will avoid the transformation that gets applied to JSX I believe. – sam256 Feb 03 '22 at 21:58

1 Answers1

0

Following up on my comment: You can save yourself a lot of complexity by simply passing the rendered component instances as children to your Question component (see example below).

If it's truly critical to restrict the type of children to an enumeration of rendered components, then you can do something like: use a nominal (branded) type derived from ReactNode for both the type of children in Question's props, and as the return type of those components in the enumeration.

TS Playground

import {
  default as React,
  useState,
  type Dispatch,
  type SetStateAction,
  type ReactElement,
  type ReactNode,
} from 'react';

export type QuestionProps = {
  children: ReactNode;
  description?: string;
  title: string;
};

export const Question = ({ children, description, title }: QuestionProps): ReactElement => {
  return (
    <div className={'question-wrapper'}>
      <div className={'question-title'}>{title}</div>
      <div className={'question-description'}>{description}</div>
      <div className={'question-component'}>{children}</div>
    </div>
  );
}

// You can use this util if you are forwarding the return type of `useState` to components
export type PropsWithForwardedState<
  State,
  Props extends Record<string, unknown>,
> = Omit<Props, 'value' | 'setValue'> & {
  value: State;
  setValue: Dispatch<SetStateAction<State>>;
};

// Your Option type (You didn't show this)
type Option <Value extends number | string> = {
  id: string;
  value: Value;
};

// Your Radio component (You didn't show this)
declare const Radio: (props: PropsWithForwardedState<Option<string>, {
  options: unknown; // You didn't show this
  maxColumns: number; // You didn't show this
}>) => ReactElement;

const Example = (): ReactElement => {
  const [optionValue, setOptionValue] = useState<Option<string>>({ value:'', id:'' });
  const [options] = useState<unknown>(); // (You didn't show this)

  return (
    <>
      <Question title="Radio:">
        <Radio
          value={optionValue}
          setValue={setOptionValue}
          options={options}
          maxColumns={3}
        />
      </Question>
      {/*

      <Question title="Checkbox:">
        <Checkbox
          value={arrValues}
          setValue={setArrValues}
          options={options}
          maxColumns={3}
        />
      </Question>

      */}
      {
        // Same for Dropdown, Rating, Text, etc.
      }
    </>
  );
};

jsejcksn
  • 27,667
  • 4
  • 38
  • 62
  • I know that this is possible, but its not what we want. We specifically want this Solution to work, as we will transfer it to different Components, that work the Same way. Such as Icons or other Component Groups. So passing them as a Child is not the right outcome – Cascade_Ho Feb 02 '22 at 21:55
  • @Cascade_Ho If this solution does not meet your needs, then perhaps you can edit your question to explain why you need to defer the render of the child component so that the real criteria can be addressed. – jsejcksn Feb 02 '22 at 22:08
  • I don't know what to edit, to be honest. The Code above works fine in JavaScript and I just want it to be transfered to TypeScript – Cascade_Ho Feb 02 '22 at 22:09
  • @Cascade_Ho In my experience, moving from JS to TS requires refactoring code in most cases. I suggest considering what I've shown to reduce complexity in your application (and allow for a more flexible pattern). Here's a playground with my work/research regarding generics: https://tsplay.dev/we4BdW – jsejcksn Feb 03 '22 at 00:16
  • I appreciate your time, but my Question was not "Can i do this in another way" It was, how to write the Function above in TypeScript Code. But that is not the case. I know my Solution works in TypeScript aswell, but I just do not know the correct Syntax and sadly the specific cases are not documented that well. – Cascade_Ho Feb 03 '22 at 00:42
  • I narrowed this case down to the Core aspects. Hope this will clarify what i need to work in TypeScript: https://stackoverflow.com/questions/70968723/typescript-complex-generics-with-components – Cascade_Ho Feb 03 '22 at 09:31