在 TypeScript 中对 React 组件进行柯里化
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
封面图片由维多利亚·史密斯绘制
首先,让我提醒你一下它的currying实际含义。
const add = (x: number) => (y: number) => x + y;
const result = add(4)(2) // 6
这就是你目前需要知道的全部信息。
让我们直奔主题。
请看这个例子:
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} />
借助Curry函数,我们可以对Result组件施加一些约束。如果您想了解如何推断['hello', 'holla', 'hi']元组,或许可以看看我之前的文章。
ToRecord递归地遍历元组中的每个元素,并将每个元素累加到记录key/value中Cache。
请不要过分关注这种实用工具类型。
看来我们还能做得更多。零部件工厂怎么样?
我在这里找到了这个例子
鉴于类型
type EnumerableComponentFactory = <C, I>(config: {
Container: React.ComponentType<C>
Item: React.ComponentType<I>;
}) => React.FC<{ items: I[] }>;
采用以下实现方式
const Enumerable: EnumerableComponentFactory =
({ Container, Item }) =>
({ items }) =>
(
<Container>
{items.map((props, index) => (
<Item key={index} {...props} />
))}
</Container>
);
和…
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" }]} />;
我花了一点时间才明白这里发生了什么。
所以,我希望你理解了主要思路。你有一个函数,它返回一个 React 函数式组件FC。该函数接收一些参数。Props返回的组件FC取决于这些参数。
不如创建一个手风琴组件,然后编写一些复杂且难以理解的类型定义?
它应该有一个带有isOpenprop 的 children 组件。每个 child 组件也是一个 React 组件,需要从父组件获取其他子组件不能使用的独特 props。isOpen每个组件都需要这个 prop。
我知道,我的需求可能有点难懂 :D。
这里符合预期行为:
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并WithTitle期望,{title: string}因此{count: number},Container应该期望{title: string, count: number}。
我们先从一些实用工具类型开始。
首先,我们需要能够从……推断属性。FC<Props>
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;
{
type Test1 = IsValid<[FC<unknown>]> // never
type Test2 = IsValid<[FC<BaseProps>]> //[React.FC<BaseProps>]
}
现在,我们需要从所有传递的组件中提取所有属性,合并它们并省略isOpen,因为我们Result不应该接受它。
// 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"
}
我们可以把它放在其他位置。
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
PS:如果您有关于如何组合 React 组件的有趣示例,请告诉我。
结束了。
文章来源:https://dev.to/captainyossarian/currying-react-components-13h0