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

Posted

技术标签:

【中文标题】在 TypeScript 中使用高阶组件在使用时省略 React 属性【英文标题】:Using higher-order components in TypeScript to omit React property on usage 【发布时间】:2018-09-17 22:29:02 【问题描述】:

我正在尝试在 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...'。 输入'你好:字符串; ' 不可分配给类型“只读”。 类型 ' 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 属性?

【问题讨论】:

类装饰器突变的类型系统没有识别。 @AluanHaddad 我已经更新了问题以询问如何使用高阶组件来代替,因为这应该会导致类型系统识别变异类。 是的,这是正确的方法,但我认为您没有正确使用Exclude&lt;T, U&gt; 【参考方案1】:

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

// 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 */
</>)

更新

2018-08-25

更新了“TPropOmit”实用程序类型以更好地处理联合类型 感谢 'vilic' 对以下 github 问题的评论: https://github.com/piotrwitek/utility-types/issues/19

【讨论】:

【参考方案2】:

您没有正确使用Exclude,exclude 将从第一个类型参数中排除第二个类型参数,因此Exclude&lt;'a'|'b', 'b'&gt; == 'a' 您可以使用ExcludekeyofPick 来省略类型中的属性:@987654326 @

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

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

所以PS 被推断为可能的最通用类型,而T 仍然是typeof MyComponent,因此重新调整的类将需要相同的道具(即使我们要更正也会发生这种情况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"
    />
  );

【讨论】:

【参考方案3】:

在 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 ... 
)

【讨论】:

以上是关于在 TypeScript 中使用高阶组件在使用时省略 React 属性的主要内容,如果未能解决你的问题,请参考以下文章

使用 graphql 将道具传递给高阶组件时出现 React-apollo Typescript 错误

通过 React 高阶组件传递子道具的正确 TypeScript 类型

TypeScript 和 React:如何重载高阶组件/装饰器?

Typescript中的装饰器原理

制作一个高阶组件以与 TypeScript 互操作 react-relay 和 react-router

来做操吧!深入 TypeScript 高级类型和类型体操