in my team's application, we have run into a few cases where it would be nice to be able to dynamically compose component wrappers (HOCs) without having to know all the wrapper interfaces ahead of time (mostly for swapping out context providers when large portions of our component tree are rendered from different host containers).
I'm able to write a simple implementation for this, but getting optimal type safety has been... challenging . In short, I'd like to be able to declare a collection of wrappers such that when any wrapper components are passed into it, the compiler will enforce that each wrapper component gets its correct props type as well.
Here's some code to illustrate the problem. Essentially, my question is how to define WrapperSet.
import * as React from "react";
export const Container: React.FC = () => {
// these could be any arbitrary wrapper components
const wrappers: WrapperSet = [
[WrapperA, { label: "WrapperA" }],
[WrapperB, { title: "foo" }],
];
const content = <span>Original content</span>;
return wrap(content, wrappers);
};
const WrapperA: React.FC<{ label: string }> = ({ label, children }) => (
<>
<div>WrapperA: {label}</div>
{children}
</>
);
const WrapperB: React.FC<{ title: string }> = ({ title, children }) => (
<>
<div>WrapperB: {title}</div>
{children}
</>
);
function wrap(children: JSX.Element, wrappers: WrapperSet): JSX.Element {
let content = children;
wrappers.forEach((wrapper) => {
const [ComponentType, props] = wrapper;
content = <ComponentType {...props}>{content}</ComponentType>;
});
return content;
}
type WrapperSet = [React.ComponentType, React.PropsWithChildren<{}>][];
If I hover over WrapperSet here, TS tells me type WrapperSet = [any, any][], which is not at all protective.
Some other definitions of WrapperSet I have tried:
type WrapperSet2 = [React.ComponentType<any>, React.PropsWithChildren<any>][] &
{
[K in number]: WrapperSet2[K] extends [React.ComponentType<infer P>, any]
? [React.ComponentType<P>, React.PropsWithChildren<P>]
: never;
};
TS interprets this as type WrapperSet2 = [any, any][] & { [x: number]: [any, any]; }, which is no better.
type Wrapper<P> = [React.ComponentType<P>, React.PropsWithChildren<P>];
type PropsFromComponentType<T extends React.ComponentType<any>> =
T extends React.ComponentType<infer P> ? P : never;
type WrapperSet3 = [any, any][] & {
[K in number]: WrapperSet4[K] extends [React.ComponentType<infer C>, any]
? Wrapper<PropsFromComponentType<C>>
: never;
};
TS says type WrapperSet3 = [any, any][] & { [x: number]: Wrapper<unknown>; }.
It seems like there must be some way to tell TS that the outer WrapperSet array supports any kinds of Wrappers, but each Wrapper must be internally consistent, based on a single props type. Or maybe TS doesn't support this kind of expression. FYI, I am using TS 4.3.
Thanks in advance!