Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React component type variance and assignability

Context

I have two components. Props of one component inherit from the props of another one.

declare namespace Props {
  interface Fruit {
    price: number;
  }

  interface Banana extends Fruit {
    curvature: number;
  }
}

declare const Fruit: React.FC<Props.Fruit>;
declare const Banana: React.FC<Props.Banana>;

The facts are:

  • React.FC is defined by React as just a function of props.
  • We know functions types are covariant in their return type and contravariant in their argument type. Source

Problem

Now, when trying to assign one to another in TypeScript 3.4.0-rc we get:

/**
 * ✅ Compile-time error. Function arguments are contravariant.
 */
const fruit: typeof Fruit = Banana;

/**
 * ✅ No error. Function arguments are contravariant.
 */
const banana: typeof Banana = Fruit;

/**
 * ✅ No errors as expected.
 */
const one: React.ReactComponentElement<typeof Fruit> = <Fruit price={3} />;

/**
 * ⁉️ No errors (but expected one). Now it's covariant.
 */
const two: React.ReactComponentElement<typeof Fruit> = <Banana price={3} curvature={15} />

/**
 * ⁉️ No errors (but expected one). Now it's contravariant.
 */
const three: React.ReactComponentElement<typeof Banana> = <Fruit price={3} />

/**
 * ✅ No errors as expected.
 */
const four: React.ReactComponentElement<typeof Banana> = <Banana price={3} curvature={15} />

Question

Why do two and three not cause an error?

like image 732
Karol Majewski Avatar asked Feb 24 '26 03:02

Karol Majewski


1 Answers

Avoiding JSX and using React.createElement solves this issue.

declare namespace Props {
  interface Fruit {
    price: number;
  }

  interface Banana extends Fruit {
    curvature: number;
  }
}

declare const Fruit: React.FC<Props.Fruit>;
declare const Banana: React.FC<Props.Banana>;

// ✅ 
const one: React.ReactComponentElement<typeof Fruit> = React.createElement(
  Fruit,
  { price: 1000 }
);

// ✅
const two: React.ReactComponentElement<typeof Banana> = React.createElement(
  Banana,
  { price: 1000, curvature: 12 }
);

/**
 * ✅ Compile-time error.
 * Type 'FunctionComponentElement<Banana>' is not assignable to type
 * 'ReactComponentElement<FunctionComponent<Fruit>, Pick<PropsWithChildren<Fruit>, "price" | "children">>'.
 * ...
 * fruits.tsx(10, 5): 'curvature' is declared here.
 */
const three: React.ReactComponentElement<typeof Fruit> = React.createElement(
  Banana
);

/**
 * ✅ Compile-time error.
 * Type 'FunctionComponentElement<Fruit>' is not assignable to type
 * 'ReactComponentElement<FunctionComponent<Banana>,
 * ...
 * Types of property 'propTypes' are incompatible. 🤔
 */
const four: React.ReactComponentElement<typeof Banana> = React.createElement(
  Fruit,
  { price: 12 }
);

But it creates another one -- we lose autocomplete on props in VSCode.

like image 68
hasparus Avatar answered Feb 26 '26 17:02

hasparus