I used Currying.
It is easier to infer all generics.
Curry
This function takes two components and thanks to variadic tuple types infers each component. More about inference you can find in my article
Component validation
Curry
function also validates each component to make sure each accepts BaseProps
.
import React, { FC } from "react";
type BaseProps = {
isOpen: boolean;
};
const WithTitle: FC<BaseProps & { title: string }> = ({ isOpen, title }) => (
<p>
Component 1: isOpen: {isOpen.toString()}. Title: {title}
</p>
);
const WithCount: FC<BaseProps & { count: number }> = ({ isOpen, count }) => (
<p>
Component 2: isOpen: {isOpen.toString()}. Count: {count}
</p>
);
type GetRequired<T> =
// make sure we have a deal with array
T extends Array<infer F>
? // make sure that element in the array extends FC
F extends FC<infer Props>
? // if Props extends BaseProps
Props extends BaseProps
? // Omit isOpen property, since it is not needed
Omit<Props, "isOpen">
: never
: never
: never;
type ExtractProps<F extends FC<any>> = F extends FC<infer Props>
? Props
: never;
type IsValid<Components extends Array<FC<BaseProps>>> =
ExtractProps<[...Components][number]> extends BaseProps ? Components : never
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
const Curry =
<Comps extends FC<any>[], Valid extends IsValid<Comps>>(
/**
* If each Component expects BaseProps,
* sections argument will evaluate to [...Comps] & [...Comps],
* otherwise to [...Comps] & never === never
*/
sections: [...Comps] & Valid
) =>
/**
* GetRequired<[...Comps]> returns a union
* when we need an intersection of all extra properties from
* all passed components
*
*/
(props: UnionToIntersection<GetRequired<[...Comps]>>) =>
(
<>
{sections.map((Comp: FC<BaseProps>) => (
<Comp isOpen={true} {...props} /> // isOpen is required
))}
</>
);
const Container = Curry([WithCount, WithTitle]);
const result = <Container title={"hello"} count={42} />; // ok
const result_ = <Container title={"hello"} count={"42"} />; // expected error
const Container_ = Curry([WithCount, () => null]); // expected error
As you might have noticed, dealing with FC<Props>
is tricky. I have used UnionToIntersection
. This is because Props
is in contravariant position to FC
.
declare var foo: FC<BaseProps & { count: number }>;
declare var baz: FC<BaseProps>;
foo = baz // ok
baz = foo // error
// REMOVE FC wrapper
declare var foo_: BaseProps & { count: number };
declare var baz_: BaseProps;
foo_ = baz_ // error
baz_ = foo_ // ok