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
- Compile without any Typescript errors
- 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.