Typescript React别名HOC(使用<component.alias>)输入问题

时间:2018-06-01 13:36:42

标签: reactjs typescript

我正在尝试创建一个HOC(我也希望能够用作装饰器)来执行以下操作:

假设我有一个名为“counter”的组件

interface ICounterProps {
  count: number;
}

interface ICounter<T> extends React.Component<T> {
  // I'd have to add all aliases here manually I guess
  startAt5?: React.SFC<ICounterProps>
}

class Counter extends React.Component<ICounterProps> implements ICounter<ICounterProps> {
  render() {
    return <div>{this.props.count}</div>
  }
}

现在我的HOC应该做类似的事情:

const alias = (name: string, setProps: {}) => (Component: any) => {
  Component[name] = (props: {}) => <Component {...setProps} {...props} />
}

所以我可以将它用作类组件的装饰器或功能组件的函数:

@alias('startAt5', { count: 5 })
class counter {...}

interface IOtherCounterProps {
  otherCount: number;
}

interface IOtherCounter extends React.SFC<IOtherCounterProps> {
  startAt10?: React.SFC<IOtherCounterProps>
}

const someOtherCounter: IOtherCounter = ({}) => {...}
alias('startAt10', { otherCount: 10 })(SomeOtherCounter);

后来使用我的组件使用不同的预设,如:

<Counter.startAt5 />
<OtherCounter.startAt10 />

代码应该像这样工作(或者像这样工作),制作这个沙箱,以便你可以尝试: https://codesandbox.io/s/ww1vrqlxw7

虽然在index.tsx中,您会看到别名创建的组件上的TS警告。而别名也没有真正打好。

所以我的问题:

  1. 您可以使用哪种类型正确键入Componentprops
  2. 您甚至可以输入setProps(在计数器上用作装饰器时使用ICounterProps)吗?
  3. 您可以动态生成ICounterIOtherCounter以包含所有别名吗?
  4. 你能否以某种方式缩短class Counter类型(仅使用一个扩展或实现)?
  5. 是否有更好的解决方案来创建预先支撑的子组件?
  6. 亲切的问候。

1 个答案:

答案 0 :(得分:2)

我不知道这个'预先支撑的子组件'是否是实现这一点的反应方式,但打字稿可以帮助点1-4。

您可以在使用时无需额外输入即可达到您想要的效果。诀窍是我们将原始组件传递给函数并使用函数的返回值,该函数将改变原始类型以正确键入额外的子组件:

// Helper type to extarct the props form a component type
type ComponentProps<T extends React.ComponentClass<any> | React.SFC<any>> = 
    T extends React.ComponentClass<infer P> ? P :
    T extends React.SFC<infer P> ? P : never;


type RestOfProperties<TAllProps, TPartial extends Partial<TAllProps>> =
    Pick<TAllProps, Exclude<keyof TAllProps, keyof TPartial>> & Partial<TPartial>;

// We need to declare 2 versions of the function that does the mutating of the type one for SCF the other for ComponentClass
// There may be a solution with a single class but I can't see it right now
    declare class Helper<TComponent extends React.ComponentClass<any>, TAllProps extends ComponentProps<TComponent>> {
    Component: TComponent;
    alias<TName extends string, TProps extends Partial<ComponentProps<TComponent>> & Record<Exclude<keyof TProps, keyof ComponentProps<TComponent>>, never> >(name: TName, setProps: TProps) : 
        Helper<TComponent & { [P in TName]: (props: RestOfProperties<TAllProps, TProps>) => JSX.Element }, TAllProps>
}
declare class HelperSCF<TComponent extends React.SFC<any>, TAllProps extends ComponentProps<TComponent>> {
    Component: TComponent;
    alias<TName extends string, TProps extends Partial<ComponentProps<TComponent>> & Record<Exclude<keyof TProps, keyof ComponentProps<TComponent>>, never>>(name: TName, setProps: TProps) : 
        HelperSCF<TComponent & { [P in TName]: (props: RestOfProperties<TAllProps, TProps>) => JSX.Element }, TAllProps>
}
// Define the aliasFactory function, with overlaods for SCF and ComponentClass
function aliasFactory<TComponent extends React.SFC<any>>(Component: TComponent) : HelperSCF<TComponent, ComponentProps<TComponent>>
function aliasFactory<TComponent extends React.ComponentClass<any>>(Component: TComponent) : Helper<TComponent, ComponentProps<TComponent>> 
function aliasFactory<TComponent extends React.ComponentClass<any> | React.SFC<any>>(Component: TComponent) {
    class Helper<T extends React.SFC<any>> {
        public constructor(public Component: T) { }
        alias<TName extends string, TProps extends Partial<ComponentProps<TComponent>>>(name: TName, setProps: TProps) : any {
            (this.Component as any)[name] = (props: {}) => <this.Component {...setProps} {...props} />
            return this;
        }
    }

    return new Helper(Component as any);
};


// Usage sample 
// Have a mix of required and optional properties 
interface IOtherCounterProps {
    otherCount: number;
    speed?: number;
    required: number;
}

// Define the implementation 
const OtherCounterImpl : React.SFC<IOtherCounterProps> = ({ otherCount }) => <div>{otherCount}</div>;

// This is the symbol we can export that will contain all our aliasses
const OtherCounter = aliasFactory(OtherCounterImpl)
    .alias("startAt10", { otherCount: 10 })
    .Component;


interface ICounterProps {
    count: number;
}

class CounterProto extends React.Component<ICounterProps> {
    render() {
        return <div>{this.props.count}</div>;
    }
}

// Similar for classes, we can add as may aliasses as we wish 
// This is the symbol we can export that will contain all our aliasses
const Counter = aliasFactory(CounterProto)
    .alias("startAt5", { count: 5})
    .alias("startAt10", { count: 10})
    // .alias("startAt10", { count: 10, notThere: ""}) // this would be an error (added after feedback)
    .Component;




let d = <div >
    <Counter count={3} />
    {/* Required properties that have been specified become optional  */}
    <Counter.startAt5  />
    <Counter.startAt10  />
    {/* Optional properties remnain optional, the required ones remain required */}
    <OtherCounter.startAt10 required={10}  />
</div>

部分游乐场link