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.
