Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing a React component as arg to a TypeScript function: impose a minimal shape of the "props" of the component

I have a TypeScript function with the signature:

function connectRRC<C extends ComponentType<any>>(component: C)

which currently accepts any component.

I'd like to accept ONLY components that have in "props" a given property. E.g. "id: string".

Doing:

function connectRRC<C extends ComponentType<{ id: string }>>(component: C)

doesn't work, cf. this explanation.

ComponentType is defined in React like:

type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;

interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
    new (props: P, context?: any): Component<P, S>;
    propTypes?: WeakValidationMap<P>;
    contextType?: Context<any>;
    contextTypes?: ValidationMap<any>;
    childContextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}

interface FunctionComponent<P = {}> {
    (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
    propTypes?: WeakValidationMap<P>;
    contextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}

Thanks in advance for any hints!

Update

@sam256 provided a partial working answer, which I found acceptable. However, I found additional use cases that don't work, w/o finding a reasonable explanation. This TS playground link shows them.

This works, as expected:

class GoodComponent2 extends React.Component<{id:string, otherProp:number} & { somethingElse: boolean }> {}
connectRRC(GoodComponent2)

But the following don't work:

class BadComponent3<P = { somethingElse: boolean }> extends React.Component<{id:string, otherProp:number} & P> {}
connectRRC(BadComponent3)

class BadComponent4<P = {}> extends React.Component<{id:string, otherProp:number} & P> {}
connectRRC(BadComponent4)
like image 734
Cristian Avatar asked Nov 05 '22 23:11

Cristian


1 Answers

I have a partial answer.

type RequiredProps = {id:string}

function connectRRC<T extends RequiredProps>(component: React.FC<T>){}

const goodComponent = (props:{id:string, otherProp:number}) => <div />
const badComponent = (props:{otherProp:number}) => <div />

connectRRC(goodComponent)
connectRRC(badComponent) //typescript error

This works pretty much as you want because Typescript is now inferring T and making sure it extends RequiredProps.

The only problem is this won't generate an error for a component passed with no props at all. This doesn't generate an error:

const noPropsComponent = (props:{})=><div />
connectRRC(noPropsComponent)

The reason is contravariance again, at least in part, though I don't completely understand what the compiler is doing here.

What appears to be happening is TS does not infer T as {} and therefore failing to satisfy the constraint (can anyone explain why not?)

Instead, it looks like it assumes T is at least RequiredProps (the constraint) and then just checks if component satisfies React.FC<RequiredProps>, which it does because RequiredProps is in the contravariant position.

I'm not sure of a good way to get past this, in part because I'm not sure I totally get why TS is behaving this way. Specifically, I don't understand why it doesn't infer T as {} and fail it for not satisfying the RequiredProps constraint.

Hopefully someone else on the forum can help...

Playground

like image 128
sam256 Avatar answered Nov 11 '22 05:11

sam256