SolvedDefinitelyTyped [@type/react] Generic Props lost with React memo

For some reason React.memo drops the generic prop type and creates a regular union type. I've posted on StackOverflow but there doesn't seem to be a non-hacky solution (despite the 100 point bounty I put on the question). The following toy example illustrates the issue:

interface TProps<T extends string | number> {
  arg: T;
  output: (o: T) => string;
}

const Test = <T extends string | number>(props: TProps<T>) => {
  const { arg, output } = props;
  return <div>{output(arg)} </div>;
};

const myArg = 'a string';
const WithoutMemo = <Test arg={myArg} output={o => `${o}: ${o.length}`} />;

const MemoTest = React.memo(Test);
const WithMemo = <MemoTest arg={myArg} output={o => `${o}: ${o.length}`} />;

The last line results in an error message for the output function's o.length:

Property 'length' does not exist on type 'string | number'.
  Property 'length' does not exist on type 'number'.

A workaround is to assign the React.memo to a generic interface:

interface MemoHelperFn {
  <T extends string | number>(arg: TProps<T>): JSX.Element;
}

but this in turn requires forcing TypeScript by adding a @ts-ignore to the assignment.

30 Answers

✔️Accepted Answer

Unless you need all the baggage, maybe just override the typings locally:

const typedMemo: <T>(c: T) => T = React.memo;
const MemoTest = typedMemo(Test);
const WithMemo = <MemoTest arg={myArg} output={o => `${o}: ${o.length}`} />;

Other Answers:

I think what you're looking for is...

const MemoizedComponent = React.memo(innerComponent) as typeof innerComponent;

I ended up using this:

import { ComponentType, memo, useEffect, ComponentProps } from 'react';

type PropsComparator<C extends ComponentType> = (
  prevProps: Readonly<ComponentProps<C>>,
  nextProps: Readonly<ComponentProps<C>>,
) => boolean;

function typedMemo<C extends ComponentType<any>>(
  Component: C,
  propsComparator?: PropsComparator<C>,
) {
  return (memo(Component, propsComparator) as any) as C;
}

function Foo<D>(props: { foo: 2, bar: D }) {
  return <div>foo</div>;
}

const Memoed = typedMemo(Foo);

/**
 * Generic remained!
 * const Memoed: <D>(props: {
 *     foo: 2;
 *     bar: D;
 * }) => JSX.Element
 */

@pie6k's solution worked for me! #37087 (comment) 🙏

@marc-ed-raffalli's (#37087 (comment)) exhibited the same issue I was having with the standard React typings likely due to the lack of generic type parameters in the function signature.

@nandorojo's solution worked by using T extends ComponentType<any> instead of T.

const typedMemo: <T extends ComponentType<any>>(
  c: T,
  areEqual?: (
    prev: React.ComponentProps<T>,
    next: React.ComponentProps<T>
  ) => boolean
) => T = React.memo;

And to make it work without variable assignment at runtime, I have modified it to work as follows:

declare module "react" {
  function memo<T extends React.ComponentType<any>>(
    c: T,
    areEqual?: (
      prev: Readonly<React.ComponentProps<T>>,
      next: Readonly<React.ComponentProps<T>>
    ) => boolean
  ): T;
}

More Issues: