Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

For a React + TypeScript variable: enforce some attributes in the "props" of a component

I'd like to have a variable (actually function argument, but I simplified the problem) that can hold a React component that MUST have e.g. aNumber: number among it's properties.

My unsuccesfull try was:

function MyComp(props: { aNumber: number, aString: string }) {
    return <>{props.aNumber} {props.aString}</>;
}

let f2: FunctionComponent<{ aNumber: number }> = MyComp;

giving me the error:

Type '(props: { aNumber: number; aString: string; }) => Element' is not assignable to type 'FunctionComponent<{ aNumber: number; }>'.
  Types of parameters 'props' and 'props' are incompatible.
    Property 'aString' is missing in type 'PropsWithChildren<{ aNumber: number; }>' but required in type '{ aNumber: number; aString: string; }'.ts(2322)

FunctionComponent is defined in react as:

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

I don't understand the error. If I defined a var w/ a constraint to have aNumber, and I assigned to it a richer element => why isn't it happy? And why it complains about those "richer" attributes?

Of course, the following works:

let f1: FunctionComponent<any> = MyComp;

Usually when I try advanced typing constructs in TS, I finally get it working, after a lot of head aches. This time it doesn't seem advanced, but I didn't manage to figure it out.

Many thanks in advance.

like image 944
Cristian Avatar asked Nov 16 '25 15:11

Cristian


1 Answers

The problem is that React components are functions and functions are contravariant, meaning that if a function with parameters of type T is required then it can be satisfied with a function of a wider type than T but not a narrower type. That is as opposed to, say, plain objects, which are covariant, i.e., that will accept a narrower type, but not a wider one.

You can see the same thing without any React code

function MyFunc(props:{a: number, b: string}){return}

let func2: (props:{a:number})=>void = MyFunc 
//   ^-- error because MyFunc is narrower than typeof func2

An intuitive way to think of this is from the perspective of the consumer calling your function/React component. In the case where they are expecting to call a function of typeof f2, they are expecting to only need to pass it props of aNumber and yet by assigning a narrower function to f2, they would now need to also pass aString, which they were not expecting.

This issue comes up a lot in React for an understandable reason. Imagine you have a <Button> component, that takes one prop, and a <FancyButton> component that takes multiple props. It's tempting to think, well, if I need any old <Button> than a <FancyButton> will do just as well. That sort of sounds like simple inheritance. But if you think about it a little more, there's an error. If I'm a component expecting to need to insert a <Button> as one of my children, that means I'm only expecting to need to pass it one prop. If I now swap in a <FancyButton> a bunch of props are now expected by that component that I wasn't expecting to provide.

The intuitive point is that you aren't just displaying the <Button> component--in which case you could just not care about the fact that the <FancyButton> has more "fanciness". It's that you actually need to provide inputs--props--to the component, and so a narrower one will require you (the parent component) to provide more.

The same logic is of course generally true for all functions, but I guess it tends to come up more often in React because we tend to think of components as "objects to display".

Here is a more expansive SO answer on this issue generally (which is tricky at first!): Difference between covariant and contravariant positions in Typescript

like image 120
sam256 Avatar answered Nov 18 '25 12:11

sam256



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!