Currying React components in TypeScript
Tags: typescript, react, javascript, functional
Published: 2021-07-20 18:36:37

_Cover image made by Victoria Smith_

First of all, let me remind you what `currying` actually means.
```typescript
const add = (x: number) => (y: number) => x + y;
const result = add(4)(2) // 6
```
This is all what you need to know for this moment.
Let's get straight to the point.

Consider this example:
```typescript
import React, { FC } from "react";

/**
 * Converts 
 * ['hello', 'holla', 'hi']
 * into
 * {hello: 0, holla: 1, hi: 2}
 * 
 */
type ToRecord<
    T extends string[],
    Cache extends Record<string, number> = {}
    > =
    T extends []
    ? Cache
    : T extends [...infer Head, infer Last]
    ? Last extends string
    ? Head extends string[]
    ? ToRecord<
        Head, Cache & Record<Last, Head['length']>
    >
    : never
    : never
    : never

const Curry = <
    Elem extends string,
    Data extends Elem[]
>(data: [...Data]): FC<ToRecord<Data>> =>
    (props) =>
        <div>{Object.keys(props).map(elem => <p>{elem}</p>)}</div>

// FC<{ greeting: string; }>
const Result = Curry(['hello', 'holla', 'hi']) 

// hello - is a required property
const jsx = <Result hello={0} holla={1} hi={2} />
```
Thanks to `Curry` function, we can apply some constraints on our `Result` component. If you are curious how to infer `['hello', 'holla', 'hi']` tuple, you might be interested in my previous article.

{% link https://dev.to/captainyossarian/typescript-type-inference-on-function-arguments-2n93 %}

`ToRecord` recursively iterates through each element in the tuple and accumulates each `key/value` in the `Cache` record.
Please don't focus too much on this utility type.

It looks like we can do more. What about component factory?

This example I found here
{% stackoverflow 68397897 %}

```typescript
import React, { FC, ComponentType } from "react";

type EnumerableComponentFactory = <I>(config: {
    Container: FC<{ children: JSX.Element[] }>;
    Item: ComponentType<I>;
}) => FC<{ items: I[] }>;

const Enumerable: EnumerableComponentFactory =
    ({ Container, Item }) =>
        ({ items }) =>
        (
            <Container>
                {items.map((props, index) => (
                    <Item key={index} {...props} />
                ))}
            </Container>
        );

const UnorderedList = Enumerable({
    Container: ({ children }) => <ul>{children}</ul>,
    Item: ({ title }: { title: string }) => <li>{title}</li>,
});

const result = <UnorderedList items={[{ title: "Something" }]} />;
```
It took me a bit to understand what's going on here.
So, I hope you understood the main idea. You have a function which returns a react functional components `FC`. First function receives some arguments. `Props` of returned `FC` depends on these arguments.

How about creating Accordeon component and writing some crazy an unreadable typings?

It should have a children with `isOpen` prop. Each child is also a React component that needs unique props from the parent that other children may not use. `isOpen` property is required for each component.
I know, it hards to understand my requirements :D.

Here you have expected behaviour:
```typescript
import React, { FC } from "react";

type BaseProps = {
    isOpen: boolean;
};

const WithTitle: FC<BaseProps & { title: string }> =
    ({ isOpen, title }) => <p>{title}</p>;

const WithCount: FC<BaseProps & { count: number }> =
    ({ isOpen, count }) => <p>{count}</p>;

const Container = Curry([WithCount, WithTitle]);

/**
 * Ok
 */
const result = <Container title={"hello"} count={42} />; // ok

/**
 * Error
 */

// because [count] is string instead of number
const result_ = <Container title={"hello"} count={"42"} />;

// because second component does not expect [isOpen] property
const Container_ = Curry([WithCount, () => null]);

```
`WithCount` and `WithTitle` expects `{title: string}` and `{count: number}` accordingly, hence `Container` should expect `{title: string, count: number}`.

Let's start with some utility types.

First of all, we need to be able to infer props from `FC<Props>`
```typescript
type ExtractProps<F extends FC<any>> = F extends FC<infer Props>
    ? Props
    : never;
{
    type Test = ExtractProps<FC<{ age: number }>> // { age: number }
}
```

Then, we need to check if every component has expected props.

```typescript
type IsValid<
    Components extends Array<FC<BaseProps>>
    > =
    ExtractProps<[...Components][number]> extends BaseProps
    ? Components
    : never;
{
    type Test1 = IsValid<[FC<unknown>]> // never
    type Test2 = IsValid<[FC<BaseProps>]> //[React.FC<BaseProps>]
}
```

Now, we need to extract all properties from all passed components, merge them and omit `isOpen`, because our `Result` should not accept it.

```typescript
// 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;

type GetRequired<T> = UnionToIntersection<
    // 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 Test = keyof GetRequired<[
        FC<BaseProps & { title: string }>,
        FC<BaseProps & { count: number }>
    ]> // "title" | "count"
}
```
We can put it all other.

```typescript
import React, { FC } from "react";

type BaseProps = {
    isOpen: boolean;
};

const WithTitle: FC<BaseProps & { title: string }> =
    ({ isOpen, title }) => <p>{title}</p>
const WithCount: FC<BaseProps & { count: number }> =
    ({ isOpen, count }) => <p>{count}</p>

// 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;

type GetRequired<T> = UnionToIntersection<
    // 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 Test = keyof GetRequired<[
        FC<BaseProps & { title: string }>,
        FC<BaseProps & { count: number }>
    ]> // "title" | "count"
}

type ExtractProps<F extends FC<any>> = F extends FC<infer Props>
    ? Props
    : never;
{
    type Test = ExtractProps<FC<{ age: number }>> // { age: number }
}

type IsValid<
Components extends Array<FC<BaseProps>>
> =
    ExtractProps<[...Components][number]> extends BaseProps 
    ? Components 
    : never;
{
    // never
    type Test1 = IsValid<[FC<unknown>]> 
    // [React.FC<BaseProps>]
    type Test2 = IsValid<[FC<BaseProps>]> 
}

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
    ) =>
        (props: GetRequired<[...Comps]>) =>
        (
            <>
                {sections.map((Comp: FC<BaseProps>) => (
                    // isOpen is required
                    <Comp isOpen={true} {...props} />
                ))}
            </>
        );

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
```

P.S. If you have some interesting examples of composing React components, please let me know.

The end.