Skip to content

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

@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
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions