在 TypeScript 中使用高阶组件在使用时省略 React 属性

IT技术 reactjs typescript
2021-05-04 01:43:17

我正在尝试在 TypeScript 中编写一个高阶组件,它接受一些 React 组件类,包装它,并返回一个省略了声明属性之一的类型。这是我尝试过的:

interface MyProps {
  hello: string;
  world: number;
}

interface MyState { }

function HocThatMagicallyProvidesProperty<P, S, T extends {new(...args:any[]): React.Component<Exclude<P, "world">, S>}>(constructor: T): T {
  throw new Error('test');
}

const MyComponent = HocThatMagicallyProvidesProperty(class MyComponent extends React.Component<MyProps, MyState> {
  constructor(props: MyProps, context: any) {
    super(props, context);
  }

  public render() {
    return <div></div>;
  }
})

function usingThatComponent() {
  return (
    <MyComponent
      hello="test"
    />
  );
}

但是,在使用组件时出现错误:

输入'{你好:字符串; }' 不可分配到类型 'IntrinsicAttributes & IntrinsicClassAttributes & Readonly<{ children?: ReactNode; }>...'。输入'{你好:字符串; }' 不可分配给类型 'Readonly'。类型“{ hello: string;”中缺少属性“world” }'。

我也试过这个 HOC 声明:

function HocThatMagicallyProvidesProperty<P, S, T extends {new(...args:any[]): React.Component<P, S>}>(constructor: T): {new(...args:any[]): React.Component<Exclude<P, "world">, S>} {
  throw new Error('test');
}

但是,这既不适用于类的使用,也不适用于实际的 HOC 调用。

如何定义高阶组件,以便world在使用类时不必传入属性?

3个回答

更简单的版本,允许包装类和函数组件。为可读性而明确命名(希望如此)

// OLD VERSION:
// export type TPropOmit <T, K extends string> = (
//   Pick<T, Exclude<keyof T, K>>
// )
// END OF CHANGE

// NEW VERSION: 2018-08-25
export type TPropOmit <T, K extends string> = (
  T extends any
    ? Pick<T, Exclude<keyof T, K>>
    : never
)
// END OF CHANGE

export namespace withSomePropsSet {
  export type TPropsInject = {
    world :number
  }
}
export function withSomePropsSet <
  TPropsOrig extends withSomePropsSet.TPropsInject,
  TPropsNew = TPropOmit<TPropsOrig, keyof withSomePropsSet.TPropsInject>
> (
  WrappedComponent :React.ComponentType<TPropsOrig>
) :React.ComponentType<TPropsNew>
{
  const injectProps :withSomePropsSet.TPropsInject = {
    world: 123,
  }
  return (props :TPropsNew) :React.ReactElement<TPropsOrig> => (
    <WrappedComponent {...injectProps} {...props} />
  )
}

和用法:

type TMyProps = {
  hello ?:string
  world  :number
}
class MyCompCls extends React.Component<TMyProps, {}> {
  public render () {
    return (<div>{this.props.hello} {this.props.world}</div>)
  }
}
const MyCompFn = (props :TMyProps) => {
  return (<div>{props.hello} {props.world}</div>)
}

const MyCompClsWrapped = withSomePropsSet(MyCompCls)
const MyCompFnWrapped  = withSomePropsSet(MyCompFn)

const UsagePreWrap = (<>
  <MyCompCls                                 /> {/* INVALID */}
  <MyCompCls  hello = 'test'                 /> {/* INVALID */}
  <MyCompCls  world = {123}                  /> {/* OK */}
  <MyCompCls  hello = 'test'  world = {123}  /> {/* OK */}

  <MyCompFn                                 /> {/* INVALID */}
  <MyCompFn  hello = 'test'                 /> {/* INVALID */}
  <MyCompFn  world = {123}                  /> {/* OK */}
  <MyCompFn  hello = 'test'  world = {123}  /> {/* OK */}
</>)

const UsagePostWrap = (<>
  <MyCompClsWrapped                                /> {/* OK*/}
  <MyCompClsWrapped hello = 'test'                 /> {/* OK */}
  <MyCompClsWrapped world = {123}                  /> {/* INVALID */}
  <MyCompClsWrapped hello = 'test'  world = {123}  /> {/* INVALID */}

  <MyCompFnWrapped                                   /> {/* OK */}
  <MyCompFnWrapped    hello = 'test'                 /> {/* OK */}
  <MyCompFnWrapped    world = {123}                  /> {/* INVALID */}
  <MyCompFnWrapped    hello = 'test'  world = {123}  /> {/* INVALID */}
</>)

更新

您没有Exclude正确使用, exclude 将从第一个参数中排除第二个类型参数,因此Exclude<'a'|'b', 'b'> == 'a'您可以使用Exclude,keyofPick从类型中省略属性:Pick<TProps, Exclude<keyof TProps, 'world'>>>

问题的第二部分是您的函数调用的类型参数不是您所期望的,悬停在 VS 代码中,我们在对 HOC 的调用中看到以下内容:

function HocThatMagicallyProvidesProperty<{}, {}, typeof MyComponent>(constructor: typeof MyComponent): typeof MyComponent

因此PS推断出最通用的类​​型{},并且 T 仍然存在typeof MyComponent,因此重新调整的类将需要相同的props(即使我们要纠正 的使用,也会发生这种情况Exclude

另一种有效的方法是使用条件类型来提取SP,并构造新的构造函数签名,不包括我们不想要的属性

type Props<T extends React.Component<any, any>> = T extends React.Component<infer P, any> ? P : never; 
type State<T extends React.Component<any, any>> = T extends React.Component<any, infer S> ? S : never;
function HocThatMagicallyProvidesProperty<T extends {new(...args:any[]): React.Component<any, any>}, 
    TProps = Props<InstanceType<T>>, 
    TState=State<InstanceType<T>>, 
    TNewProps = Pick<TProps, Exclude<keyof TProps, 'world'>>>(constructor: T) : 
(p: TNewProps) => React.Component<TNewProps, TState>  {
  throw "";

}

interface MyProps {
  hello: string;
  world: number;
}

interface MyState { }

const MyComponent = HocThatMagicallyProvidesProperty(class extends React.Component<MyProps, MyState> {
  constructor(props: MyProps, context: any) {
    super(props, context);
  }

  public render() {
    return <div></div>;
  }
})

function usingThatComponent() {
  return (
    <MyComponent
      hello="test"
    />
  );
}

在 TypeScript Gitter 上的一些人的帮助下,我们最终得到了这个解决方案:

import { Dissoc } from 'subtractiontype.ts';

export interface ValidationProps {
  onValidationStateChange: (valid: boolean) => void;
}

type OuterProps<P> = Dissoc<P, keyof ValidationProps>;

export function IsValidatableComponent<P extends ValidationProps>(WrappedComponent: React.ComponentClass<P>): React.ComponentType<OuterProps<P>> {
  // ...
}

// usage:

export const RulesetFieldEditor = IsValidatableComponent(class RulesetFieldEditor extends ... {
})