Skip to content

React's ComponentProps type issues in TypeScript 5.6.2Β #59937

Open
@jakubmazanec

Description

@jakubmazanec

πŸ”Ž Search Terms

react, forwardRef, component props, ComponentProps

πŸ•— Version & Regression Information

  • This changed between versions 5.5.4 and 5.6.2 - but in version 5.5.4 there are other related bugs πŸ€·β€β™‚οΈ

⏯ Playground Link

https://www.typescriptlang.org/play/?jsx=4&ts=5.6.2#code/JYWwDg9gTgLgBAbwLACg5xgTzAUzgUQBscQcA7GAFWxwBpV0tc4BhCcCM8mABSgjABnemgw1W7SFwp8BggOrAYACwgBXGACUcAMxGNxbDtKo19cHdADuAQygATbTrg3BcAEaucAMWt3HuuZMeL5Qtg5O2mT2OFDeamQAxjDAnEHiTiIAvhb8IHAA5FA4NskFANyoqDoJyalkFn4RugA8lLRwPAB8ABQMcImSnNwAXHA9YPxCYzwdxTpjTm1dAJRwALxdcNqlMAB0O8kAchAxIitjE1OCM3AAZIjzAPyLrZRdWWub2yXJB78wE4xRD9YowNRQBqeQQ+JoBHR9UToQbGbguNwJADWZAgVgarjgoXC8KiMTitRSnBaWJxeI6NNxZC65hWlRQWSqKGCElRFEUKiJ-icsiEbTgOAAHjByPY3EQSNxqLgtut+kYpNwRQolKoNEt3vcQUiBoRXIIjjZSC84IIYFBgGQAOZs9DoVzWyhwAA+cASMR0Dpw9hdcCybNQgzItp5Gr5OsFzWc60aYSFukR6DFkul0TlxFIFCVeGTBUEYBsZAKvX66EmcjG6uGcYFcOF12W5nQ81eOhaAAlKABZAAy8oLMGZ-S+W2QxrBEIaZDUhEIIay53DXPElBwtuWGzgnuzMrcCBRsZg1odOliMabMA5oied5M4qlJ5fipoLQrmC6NbgZ9GxMLUWmA7h-2NMYuAAN1ifpoJwOCoE3bltEEZcYAARgPcCZHbYIIGcPCYH5ZQE3hLpyjgAB6GiMGUPBigwwh4GANwrBwYAHGoqxlEwOAAAMAHkQCUH8yEwDoil0KtBKeVA0N3TCACYDx3PczyGEwxkI4jtO4MiKKcLIqNo+iVCY5TWLgdiBmgYpkjGc971LOBoGAR0HRsQg4DrIQ4AAajgeY-KmPZUCAA

πŸ’» Code

I noticed that when using custom forwardRef function (I use it so my generic components are typed correctly), I get different results in these two situations:

import {
  type ElementType,
  type ComponentProps,
  type ComponentPropsWithoutRef,
  type ComponentType,
  forwardRef as baseForwardRef,
  type ForwardRefRenderFunction,
  type Ref,
} from 'react';

// setup code
function forwardRef<T, P>(
  component: (props: P, ref: Ref<T>) => React.ReactNode,
): (props: P & {ref?: Ref<T>}) => React.ReactNode {
  return baseForwardRef(
    component as unknown as ForwardRefRenderFunction<unknown, unknown>,
  );
}

type FooProps<T extends ElementType> =
  ComponentPropsWithoutRef<T> & {
    className?: string;
    as?: T | undefined;
  };

const Foo = forwardRef(
  <T extends ElementType = 'span'>(
    props: FooProps<T>,
    ref: Ref<HTMLElement>,
  ) => {
    return null;
  },
);

type Test<T> = T extends infer Component
  ? Component extends ComponentType<any>
    ? ComponentProps<Component>
    : never
  : never;

type Result1 = ComponentProps<typeof Foo>; // the result is weird: why `Omit<any, 'ref'>`?
type Result2 = Test<typeof Foo>; // the result is correct: Foo's original props + ref prop.
import {
  type ElementType,
  type ComponentProps,
  type ComponentPropsWithoutRef,
  type ComponentType,
  forwardRef as baseForwardRef,
  type ForwardRefRenderFunction,
  type Ref,
} from 'react';

function forwardRef<T, P>(
  component: (props: P, ref: Ref<T>) => React.ReactNode,
): (props: P & {ref?: Ref<T>}) => React.ReactNode {
  return baseForwardRef(
    component as unknown as ForwardRefRenderFunction<unknown, unknown>,
  );
}

type FooProps<T extends ElementType> =
  ComponentPropsWithoutRef<T> & {
    className?: string;
    as?: T | undefined;
  };

const Foo = forwardRef(
  <T extends ElementType = 'span'>(
    props: FooProps<T>,
    ref: Ref<HTMLElement>,
  ) => {
    return null;
  },
);

type Test<T> = T extends infer Component
  ? Component extends ComponentType<any>
    ? ComponentProps<Component>
    : never
  : never;

// ⚠️ different results
type Result1 = ComponentProps<typeof Foo>; // the result is weird: why `Omit<any, 'ref'>`?
type Result2 = Test<typeof Foo>; // the result is correct: Foo's original props + ref prop.

πŸ™ Actual behavior

Type Result1 is wrong:

type Result1 = Omit<any, "ref"> & {
  className?: string;
  as?: ElementType | undefined;
} & {
  ref?: Ref<HTMLElement> | undefined;
}

and Result2 is correct:

type Result2 = PropsWithoutRef<ComponentProps<T>> & {
  className?: string;
  as?: T | undefined;
} & {
  ref?: Ref<HTMLElement> | undefined;
}

πŸ™‚ Expected behavior

Types Result1 and Result2 are same:

type Result = PropsWithoutRef<ComponentProps<T>> & {
  className?: string;
  as?: T | undefined;
} & {
  ref?: Ref<HTMLElement> | undefined;
}

Additional information about the issue

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScript

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions