在 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<T, U>
【参考方案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<'a'|'b', 'b'> == 'a'
您可以使用Exclude
、keyof
和Pick
来省略类型中的属性:@987654326 @
问题的第二部分是你的函数调用的类型参数不是你所期望的,悬停在我们在调用 HOC 时看到的 VS 代码中:
function HocThatMagicallyProvidesProperty<, , typeof MyComponent>(constructor: typeof MyComponent): typeof MyComponent
所以P
和S
被推断为可能的最通用类型,而T 仍然是
typeof MyComponent
,因此重新调整的类将需要相同的道具(即使我们要更正也会发生这种情况Exclude
的用法)
另一种可行的方法是使用条件类型来提取 S
和 P
,并构造新的构造函数签名,排除我们不想要的属性
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:如何重载高阶组件/装饰器?