DEV Community

loading...
Cover image for Currying React components in TypeScript

Currying React components in TypeScript

yossarian
Originally published at catchts.com Updated on ・4 min read

Cover image made by Victoria Smith

First of all, let me remind you what currying actually means.

const add = (x: number) => (y: number) => x + y;
const result = add(4)(2) // 6
Enter fullscreen mode Exit fullscreen mode

This is all what you need to know for this moment.
Let's get straight to the point.

Consider this example:

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} />
Enter fullscreen mode Exit fullscreen mode

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.

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

Typing a React Component Factory Function

5

Given the type

type EnumerableComponentFactory = <C, I>(config: {
  Container: React.ComponentType<C&gt
  Item: React.ComponentType<I>;
}) => React.FC<{ items: I[] }>;

with the following implementation

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

and…


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" }]} />;
Enter fullscreen mode Exit fullscreen mode

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:

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]);

Enter fullscreen mode Exit fullscreen mode

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>

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

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

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>]
}
Enter fullscreen mode Exit fullscreen mode

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

// 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"
}
Enter fullscreen mode Exit fullscreen mode

We can put it all other.

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
Enter fullscreen mode Exit fullscreen mode

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

The end.

Discussion (4)

Collapse
lishine profile image
Pavel Ravits • Edited

What is the use case?
And seems like you loose - you put it in an array and then it is not clear to whom each belonge ...

Collapse
captainyossarian profile image
yossarian Author

What I need to put in array?

Collapse
lishine profile image
Pavel Ravits

I mean, You put put params in an array and so it is not clear to which param each item belong. Again, what is the benefit of this.
First thing in the article I would like to see purpose and benefit.

Thread Thread
captainyossarian profile image
yossarian Author

Please see original question on stackoverflow. Each componene receives all properties, but uses only a part from them. As for the benefits. I wanted to show how we can make basic validation of react components and their props. Also I wanted to show that sometimes it is tricky to work with FC<Props> type because of contravariance. If above examples are not useful for you, it is perfectly fine. Everybody has his own code style, guide, preferences etc...