'How to properly write types for an array of generic React components and their properties?
I'm trying to properly type the following scenario:
I have a manager/service that contains a collection of items, each item itself consisting of a React component and it's Props.
A method on the manager class takes a Component and the Props as parameters and adds it to the collection.
A third component subscribes to the collection and renders each component with it's props.
Each component that is to be added to the collection have some common properties including some that have generic types.
The code (without types to simply convey the idea) is something like this:
// ModalService.ts
class ModalService {
modals = [];
addModal(component, props) {
this.modals.push({ component, props });
}
}
// ModalManager.tsx
const ModalManager = () => {
const modalService = useInjector(ModalService); // our custom DI injector
return (
<div className="modal-collection">
{modalService.modals.forEach(({ Component, props }) => {
<Component {...props} />
})}
</div>
)
}
// AboutModal.tsx
const AboutModal = (props) => {
return (
<div className={`about-modal ${props.show ? 'is-visible' : ''}`}>
Hi {props.name}, welcome to this bizarre example
<button onClick={() => props.onHide(true)}>
Close
</button>
</div>
)
}
// RandomNumber.tsx
const RandomNumberModal = (props) => {
const randomNum = Math.round(Math.random() * (props.max - props.min)) + props.min;
return (
<div className={`random-modal ${props.show ? 'is-visible' : ''}`}>
Today's random number is: {randomNum}
<button onClick={() => props.onHide(randomNum)}>
Close
</button>
</div>
)
}
// elsewhere in the app.tsx?
modalService.addModal(AboutModal, {
show: false,
onHide: () => void
})
modalService.addModal(RandomNumberModal, {
show: true,
onHide: (num) => saveResult(num),
min: 500,
max: 1000
})
The problem I'm stuck with is coming up with properly typed definitions for the ModalService members when TypeScript is running strict mode. Right now, my types look like this:
interface SharedModalProps<TResult> {
show: boolean;
onHide?: (result: TResult) => void;
}
interface AboutModalProps extends SharedModalProps<boolean> {
name: string;
}
interface RandomNumberModalProps extends SharedModalProps<number> {
min: number;
max: number;
}
interface ModalItem {
// cant use ComponentType<SharedModalProps<TResult>> here because the type parameter for
// TResult can be anything here
// Ideally, I should be able to do something like ComponentType<T extends SharedModalProps<TResult>> or something similar
Component: React.ComponentType;
props: ComponentProps<ModalItem['Component']>;
}
export type ModalsArray = ModalItem[];
export type AddModalFn = <TModal extends React.ComponentType>(
component: TModal,
props: React.ComponentProps<TModal>
) => void;
With this configuration, the compiler complains about the modal components being passed to the addModal(component, props)
function being incompatible with the defined type.
If I disable strict mode in tsconfig.json
, then this error goes away and the correct types are resolved for the props parameter and completions for them work as well. But disabling Strict mode isn't really a solution.
What would be the correct way to type these components and the service so the compiler is able to infer the correct types and the editor is able to offer proper completions etc. as well?
I have this sample code up and running in a Code Sandbox here for reference.
Solution 1:[1]
Try this type. All errors now shown
export default class ModalManagerService {
@observable
modals: ModalItem[] = [];
addModal<TProps, TModal extends React.ComponentType<TProps>>(Component: TModal & React.ComponentType<TProps>, props: TProps) {
this.modals.push({ Component, props });
// expect props to have the shared types from SharedModalProps
}
}
I use this for reference: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-router/index.d.ts
Solution 2:[2]
If you don't need to keep track of the props, you could just store wrappers of the components you need to render instead of storing the component and props separately.
Service
interface ModalItem {
Component: React.ComponentType;
}
...
addModal(Component: React.ComponentType) {
this.modals.push({ Component });
}
...
App
...
service.addModal(() => (
<AboutModal show name="Sierra-117" onHide={(result: boolean) => console.log('test')} />
));
...
ModalManager
{managerService.modals.map(({ Component }, index) => {
return <Component />;
})}
Solution 3:[3]
here is your code with types, it validates that you pass the correct props as well. You can do the same with interfaces if you prefer them. and link to ts playground.
type ModalProps<T> = {
show: boolean,
onHide: (val: T) => void
}
class ModalService {
modals: Array<{Component:React.FunctionComponent<any>, props: ModalProps<any>}> = [];
addModal<T extends ModalProps<any>>(Component: React.FunctionComponent<T>, props: T) {
this.modals.push({ Component, props });
}
}
const modalService = useInjector(ModalService); // our custom DI injector
const ModalManager = () => {
return (
<div className="modal-collection">
{modalService.modals.map(({ Component, props }) => {
return <Component {...props} />
})}
</div>
)
}
const AboutModal = (props: {name: string} & ModalProps<boolean>) => {
return (
<div className={`about-modal ${props.show ? 'is-visible' : ''}`}>
Hi {props.name}, welcome to this bizarre example
<button onClick={() => props.onHide(true)}>
Close
</button>
</div>
)
}
const RandomNumberModal = (props: {max: number, min: number} & ModalProps<number>) => {
const randomNum = Math.round(Math.random() * (props.max - props.min)) + props.min;
return (
<div className={`random-modal ${props.show ? 'is-visible' : ''}`}>
Today's random number is: {randomNum}
<button onClick={() => props.onHide(randomNum)}>
Close
</button>
</div>
)
}
modalService.addModal(AboutModal, {
onHide: () => {},
show: false,
name: 'asdf',
});
function useInjector(Service: typeof ModalService) {
return new Service();
}
Solution 4:[4]
you can install
@types/react
as dependency in codesandbox.after that, your mentioned error appears. (sandbox)
the errors says the type of
AboutModal
isFC<AboutModalProps>
(a functional component with props of typeAboutModalProps
), but it's not assignable to typeReact.ComponentType<{}>
(a react component with unspecified prop types).one way is to accept any prop types in your component:
// CHANGE:
Component: React.ComponentType
// TO:
Component: React.ComponentType<any> // accept component with any props
With adding <any>
(as component props), the component accepts the props of type AboutModalProps
and the error is gone. (sandbox)
- Now, we can replace
any
with a more specific type likeallModalProps
:
export interface allModalProps extends SharedModalProps<any> {
// put all of the possible modal props here.
name: string;
min: number;
max: number;
}
Now let's use the above declaration instead of React.ComponentType<any>
:
Component: React.ComponentType<allModalProps> // replaced any with allModalProps
This way, we defined a modal props interface, and we used this interface as component props, instead of any
. and we have no type errors.
Here is the sandbox: Sandbox
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Фарид Ðхмедов |
Solution 2 | diedu |
Solution 3 | Alissa |
Solution 4 | yaya |