0

What is the T type argument in ReactElement? I can tell from the body it's used for type,

interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
  type: T;
  props: P;
  key: Key | null;
}

but I am not sure what I would put in there. For example, if I have a higher-order component (HOC) that does nothing but forwards the references:

import { ComponentType, forwardRef, NamedExoticComponent, PropsWithoutRef, ReactElement, Ref, RefAttributes } from 'react';
import { render } from '@testing-library/react-native';
import { Text, TextProps } from 'react-native';

/**
 *
 * @param Component component to wrap
 * @param options options for the HoC building
 * @typeParam P the exposed props of the higher order component
 * @typeParam Q the props for the wrapped component
 * @typeParam T type for ref attribute of the wrapped component
 * @typeParam O options for the HoC building
 * @returns A named exotic componentwith P props that accepts a ref
 */
function hoc<P, Q, T, O = {}>(Component: ComponentType<Q>, options?: O): NamedExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> {
    function wrapped(props: P, ref: Ref<T>): ReactElement<Q> {
        // The unknown as Q here is an example, but P and Q can be different.
        const componentProps: Q = props as unknown as Q;
        return <Component {...componentProps as Q} ref={ref} />
    }
    const displayName =
        Component.displayName || Component.name || "AnonymousComponent";
    wrapped.displayName = displayName;
    return forwardRef<T, P>(wrapped);
}

describe("hoc", () => {
    it("should work with text", () => {
        const HocText = hoc<TextProps, TextProps, typeof Text>(Text);
        const { toJSON } = render(<HocText>simple string</HocText>);
        const { toJSON: expectedToJSON } = render(<Text>simple string</Text>)
        expect(toJSON()).toStrictEqual(expectedToJSON())
    });
});

The above works, but what's a more restrictive version I can put in ReactElement<Q,???>, but still make it compile?

Archimedes Trajano
  • 35,625
  • 19
  • 175
  • 265
  • 1
    (At least [one answer](https://stackoverflow.com/questions/74697170/what-is-the-t-type-argument-in-reactelement/74697285#74697285) (now deleted) was generated by [ChatGPT](https://meta.stackoverflow.com/questions/421831/temporary-policy-chatgpt-is-banned/421867#421867).) – Peter Mortensen Dec 11 '22 at 23:16

1 Answers1

1

T is either one of the built-in JSX elements or a custom functional/class component. For React stuff, like div, input, etc. Read parameters for *createElement().

type: The type argument must be a valid React component type. For example, it could be a tag name string (such as 'div' or 'span'), or a React component (a function, a class, or a special component like Fragment).

I believe something like this should suit your needs. I changed several things

  • ReactElement<Q> -> ReactElement<Q, typeof Component>
  • P extends Q in generic
    • This one is totally optional, since React will discard extraneous properties anyway
  • wrapped function to wrapped anonymous function will allow us to type this
    • Allows wrapped.displayName = 'foobar' to still work but disallows stuff like wrapped.foobar = true
function hoc<P extends Q, Q, T, O = {}>(
  Component: ComponentType<Q>,
  options?: O
): NamedExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> {
  const wrapped: ForwardRefRenderFunction<T, P> = (
    props: P,
    ref: Ref<T>
  ): ReactElement<Q, typeof Component> => {
    return <Component {...props} ref={ref} />;
  };
  const displayName =
    Component.displayName || Component.name || 'AnonymousComponent';
  wrapped.displayName = displayName;
  return forwardRef<T, P>(wrapped);
}

Now, as far as I know, there is no/little point to typing the return of any functional component, because JSX is not statically analyzed by TypeScript beyond knowing that it is a JSX element.

const test = <Component {...props} ref={ref} />
// ^ regardless of any component, as long it is is JSX it will
// be of type `JSX.Element`, which is just `React.ReactElement<any, any>`

So you can end up using the incorrect ReactElement anyway...

  const wrapped: ForwardRefRenderFunction<T, P> = (
    props: P,
    ref: Ref<T>
  ): ReactElement<Q, typeof Component> => {
    return <></>; // no errors?
  }

So this is just as suitable:

function hoc<P extends Q, Q, T, O = {}>(
  Component: ComponentType<Q>,
  options?: O
): NamedExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> {
  const wrapped: ForwardRefRenderFunction<T, P> = (
    props: P,
    ref: Ref<T>
  ): JSX.Element => {
    return <Component {...props} ref={ref} />;
  };
  const displayName =
    Component.displayName || Component.name || 'AnonymousComponent';
  wrapped.displayName = displayName;
  return forwardRef<T, P>(wrapped);
}

// And even more strict
function hoc<
  P extends Q,
  Q extends {},
  T extends string | JSXElementConstructor<any> =
    | string
    | JSXElementConstructor<any>,
  O = {}
>(
  Component: ComponentType<Q>,
  options?: O
): NamedExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> {
  const wrapped: ForwardRefRenderFunction<T, P> = (
    props: P,
    ref: Ref<T>
  ): JSX.Element => {
    return <Component {...props} ref={ref} />;
  };
  const displayName =
    Component.displayName || Component.name || 'AnonymousComponent';
  wrapped.displayName = displayName;
  return forwardRef<T, P>(wrapped);
}

Anyway, here's a good Q&A: When to use JSX.Element vs ReactNode vs ReactElement? with further reading.

You could use React.createElement instead of JSX as well, but there isn’t any difference aside from typing depth. Besides it'll be more of a headache than it is worth, since these are all implementation details for you and not any end user.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Cody Duong
  • 2,292
  • 4
  • 18