【问题标题】:How can I abstract passing repeated children properties while keeping types non-optional?如何在保持类型非可选的同时抽象传递重复的子属性?
【发布时间】:2018-07-04 06:11:12
【问题描述】:

我有一个组件连续多次使用,具有一些相同的属性和一些独特的属性:

interface InsideComponentProps {
    repeatedThing: string;
    uniqueThing: string;
}

const InsideComponent: React.SFC<InsideComponentProps> = ({ repeatedThing, uniqueThing }) => (
    <div>{repeatedThing} - {uniqueThing}</div>
);

const Example = () => (
    <div>
        <InsideComponent repeatedThing="foo" uniqueThing="1" />
        <InsideComponent repeatedThing="foo" uniqueThing="2" />
        <InsideComponent repeatedThing="foo" uniqueThing="3" />
    </div>
);

重复的repeatedThing 属性困扰着我,所以我正在寻找一种方法来消除这种冗余。我在非 TypeScript 应用程序中做过的一件事是引入了一个包装器组件,它可以克隆所有子级,并在此过程中添加重复的属性:

interface OutsideComponentProps {
    repeatedThing: string;
}

const OutsideComponent: React.SFC<OutsideComponentProps> = ({ repeatedThing, children }) => (
    <div>
        {React.Children.map(children, (c: React.ReactElement<any>) => (
            React.cloneElement(c, { repeatedThing })
        ))}
    </div>
);

const Example = () => (
    <OutsideComponent repeatedThing="foo">
        <InsideComponent uniqueThing="1" />
        <InsideComponent uniqueThing="2" />
        <InsideComponent uniqueThing="3" />
    </OutsideComponent>
);

生成的 JavaScript 代码具有我想要的行为,但 TypeScript 编译器出现错误,因为我在实例化 InsideComponent 时没有传递所有必需的属性:

ERROR in [at-loader] ./src/index.tsx:27:26
    TS2322: Type '{ uniqueThing: "1"; }' is not assignable to type 'IntrinsicAttributes & InsideComponentProps & { children?: ReactNode; }'.
  Type '{ uniqueThing: "1"; }' is not assignable to type 'InsideComponentProps'.
    Property 'repeatedThing' is missing in type '{ uniqueThing: "1"; }'.

我想到的唯一解决方案是将InsideComponents repeatedThing 属性标记为可选,但这并不理想,因为该值必需的。

如何保持严格性,确保InsideComponent 确实收到所有道具,同时减少调用站点上的属性重复?

我正在使用 React 16.2.0 和 TypeScript 2.6.2。

【问题讨论】:

  • 请注意,指定回调参数类型,虽然不是这里的原因,但会产生尴尬的类型错误并隐藏真正的错误。 map(components, (c: React.ReactElement&lt;any&gt;) =&gt; ...) 应该是 map, components, c =&gt; ...)
  • @AluanHaddad 我从How to assign the correct typing to React.cloneElement when giving properties to children? 收集到的;你是说答案不正确?
  • 不,我并不是说答案不正确。他正在使用类型断言c as ReactElement&lt;any&gt;,在必要时这是一种很好的风格,就像那个答案一样。您将类型断言隐藏在 (c: ReactElement&lt;any&gt;) =&gt; 后面。这是一种不好的风格,因为您实际上使用了一个断言as,但我们只有通过阅读map 的定义才能知道这一点。

标签: reactjs typescript


【解决方案1】:

TypeScript 检查以确保您将所有必需的属性分配给 React 元素。由于您在 OutsideComponent 中分配了额外的属性,因此编译器无法真正检查。

一种选择是将孩子指定为一个函数,该函数将额外的属性作为参数并将它们传播到InsideComponent。语法有点复杂,但类型更安全:

interface OutsideComponentProps {
    repeatedThing: string;
    children: (outerProps: OutsideComponentProps) => React.ReactElement<any>;
}

const OutsideComponent: React.SFC<OutsideComponentProps> = (o) => o.children(o);

const Example = () => (
    <OutsideComponent repeatedThing="foo">{(o) => 
        <div>
            <InsideComponent uniqueThing="1" {...o} />
            <InsideComponent uniqueThing="2" {...o} />
            <InsideComponent uniqueThing="3" {...o} />
        </div>
    }</OutsideComponent>
);

似乎OutsideComponent 非常抽象,因此可以重用;有什么方法可以将它转换为一个非常通用的组件,该组件接受所有的 props 并将它们作为参数提供,而不必为每个案例定义一个 OutsideComponentProps

虽然您可以将泛型函数用作组件,但您不能显式指定类型参数,它们只能被推断。这是一个缺点,但最终可以解决。

function GenericOutsideComponent<T>(props: { children: (o: T) => React.ReactElement<any> } & Partial<T>, context?: any): React.ReactElement<any> {
    return props.children(props as any);
}

const Example = () => (
    <GenericOutsideComponent repeatedThing="foo">{(o: InsideComponentProps) =>
        <div>
            <InsideComponent uniqueThing="1" {...o} />
            <InsideComponent uniqueThing="2" {...o} />
            <InsideComponent uniqueThing="3" {...o} />
        </div>
    }</GenericOutsideComponent>
);

与您的原始 JavaScript 解决方案类似,存在未指定 InsideComponent 的某些必需属性的风险,因为 GenericOutsideComponent 的属性是 Partial&lt;T&gt;(只允许指定 repeatedThing)和 @987654331 @ 是 T,否则编译器会认为 repeatedThing 未指定,并且会在 InsideComponent 上要求它。如果在InsideComponent 上设置一个虚拟值没有问题,只需将children 的签名更改为(o: Partial&lt;T&gt;) =&gt; React.ReactElement&lt;any&gt;,但这并不理想。

另一个选项是使用Pick 明确说明GenericOutsideComponent 上的哪些属性:

function GenericOutsideComponent<T>(props: { children: (o: T) => React.ReactElement<any> } & T, context?: any): React.ReactElement<any> {
    return props.children(props);
}

const Example = () => (
    <GenericOutsideComponent repeatedThing="foo">{(o: Pick<InsideComponentProps, "repeatedThing">) =>
        <div>
            <InsideComponent uniqueThing="1" {...o} />
            <InsideComponent uniqueThing="2" {...o} />
            <InsideComponent uniqueThing="3" {...o} />
        </div>
    }</GenericOutsideComponent>
);

【讨论】:

  • 我没有想过使用children的函数!似乎OutsideComponent 非常抽象,因此可以重用;有什么方法可以将它转换成一个非常通用的组件,该组件接受它的所有 props 并将它们作为参数提供,而不必为每个案例定义一个 OutsideComponentProps
  • @Shepmaster 我想我想出了一个可行的通用解决方案,看看。赏金绝对是一种动力:P
【解决方案2】:

我是 TypeScript 的新手,但这可能是另一种选择:

// OutsideComponent.tsx
import * as React from "react";

const OutsideComponent: React.SFC<{ children?: React.ReactNode, [rest: string]: any }> = (props) => {
    const { children, ...rest } = props;

    return (
        <div>
            {React.Children.map(children, ((child, i) =>
                React.cloneElement(child as React.ReactElement<any>, { key: i, ...rest }))
            )}
        </div>
    )
};

type Sub<
    O extends string,
    D extends string
    > = {[K in O]: (Record<D, never> & Record<string, K>)[K]}[O]

export type Omit<O, D extends keyof O> = Pick<O, Sub<keyof O, D>>

export default OutsideComponent;

Omit 类型取自this answer

然后

import OutsideComponent, { Omit } from './OutsideComponent';

const Example = (): React.ReactElement<any> => {
    const PartialInsideComponent: React.SFC<Omit<InsideComponentProps, 'repeatedThing'>> = InsideComponent;

    return (
        <OutsideComponent repeatedThing="foo">
            <PartialInsideComponent uniqueThing="1" />
            <PartialInsideComponent uniqueThing="2" />
            <PartialInsideComponent uniqueThing="3" />
        </OutsideComponent>
    )
};

【讨论】:

    【解决方案3】:

    我想出了一个完整的解决方案,它可以满足我的需求,同时仍然使用我拥有的 VSCode 设置提供类型安全和代码提示。

    Partialize.tsx

    import React, { cloneElement } from "react";
    
    /**
     * This HOC takes a `Component` and makes some of it's props optional as defined by `partialProps`
     *
     * @param Component The component to make some props optional
     * @param partialProps The properties to make partial
     */
    export const withPartialProps = <
      TProps extends {},
      TKeys extends (keyof TProps)[]
    >(
      Component: React.ComponentType<TProps>,
      partialProps: TKeys
    ) => {
      type TPartialProps = typeof partialProps[number];
    
      return (
        props: Omit<TProps, TPartialProps> & Partial<Pick<TProps, TPartialProps>>
      ) => {
        partialProps.forEach((propName) => {
          if (props[propName] === undefined) {
            throw Error(`${propName} is undefined`);
          }
        });
    
        return <Component {...(props as TProps)} />;
      };
    };
    
    /**
     * This HOC takes a `Component` and returns two components, the partial version created by `withPartialProps`,
     * and a wrapper component that will provide the now optional props to the child elements;
     *
     * @param Component The component to partialize
     * @param partialProps The properties to make partial
     *
     * @see withPartialProps
     */
    export const partialize = <TProps extends {}, TKeys extends (keyof TProps)[]>(
      Component: React.ComponentType<TProps>,
      partialProps: TKeys
    ) => {
      type TPartialProps = typeof partialProps[number];
      type TChildProps = Omit<TProps, TPartialProps> &
        Partial<Pick<TProps, TPartialProps>>;
      type TWrapperProps = Pick<TProps, TPartialProps> & {
        children:
          | React.ReactElement<TChildProps>[]
          | React.ReactElement<TChildProps>;
      };
    
      return {
        Partial: withPartialProps(Component, partialProps),
        PartialWrapper: ({ children, ...props }: TWrapperProps) => (
          <>
            {React.Children.map(children, (child) =>
              cloneElement(child, props as any)
            )}
          </>
        )
      };
    };
    
    

    Demo.tsx

    import React from "react";
    import { partialize } from "./Partialize";
    
    type FooProps = {
      x: number;
      y: string;
      z: "a" | "b" | "c";
    };
    
    const Foo = ({ x, y, z }: FooProps) => (
      <p>
        x: {x} | y: {y} | z: {z}
      </p>
    );
    
    export const Bar = partialize(Foo, ["y"]);
    export const Baz = partialize(Foo, ["z"]);
    

    使用示例

    <Baz.PartialWrapper z="b">
      <Baz.Partial x={1} y="Text #1" />
      <Baz.Partial x={2} y="Text #2" />
    </Baz.PartialWrapper>
    

    CodeSandbox link

    最终结果是一些比较杂乱的类型,但应该能满足问题的需要。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-06-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多