Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React's ComponentProps type issues in TypeScript 5.6.2 #59937

Open
jakubmazanec opened this issue Sep 10, 2024 · 11 comments Β· May be fixed by #59972
Open

React's ComponentProps type issues in TypeScript 5.6.2 #59937

jakubmazanec opened this issue Sep 10, 2024 · 11 comments Β· May be fixed by #59972
Labels
Bug A bug in TypeScript
Milestone

Comments

@jakubmazanec
Copy link

πŸ”Ž 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

@Andarist
Copy link
Contributor

Result2 is not correct. It leaks T and that shouldn't happen. It's the other result that is correct, you can double-check that on 5.4 by checking the types resolved based on the result of your custom forwardRef call and based on the results of "the same" inlined type (I copy-pasted its computed definition): TS playground

As we can see here, all 4 results are:

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

If we now switch this playground to 5.6.2 then we'll see one of those going rogue (the one that you assumed is correct): TS playground

@Andarist
Copy link
Contributor

A reasonable self-isolated repro: TS playground

@jakubmazanec
Copy link
Author

Huh, interesting. Thank you for the investigation.

@Alavrgajesus
Copy link

`` ceb4951646b978c517240e7bf43517a53d969c25

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Sep 11, 2024
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Sep 11, 2024
@RyanCavanaugh
Copy link
Member

We'd really need a much shorter repro to be able to prioritize this

@Andarist
Copy link
Contributor

additional repro:

function withP3<P>(p: P) {
  const m =
    <I,>(from: I) =>
    <I2,>(from2: I2) => ({ ...from, ...from2, ...p });
  return createTransform(m);
}

const addP3 = withP3({ a: 1 });
const addedSome3 = addP3({ b: '' });
const added3 = addedSome3({ c: true });

const addP3_other = withP3({ foo: 'bar' });
const addedSome3_other = addP3_other({ qwerty: 123 });
const added3_other = addedSome3_other({ bazinga: true });

We can see here { a: number } leaking from the first "chain" of instantiations into the second "chain".

I have a fix for both here: #59972 :)

@jakubmazanec
Copy link
Author

Great job as usual, thank you @Andarist!

@Alavrgajesus
Copy link

Alavrgajesus commented Sep 17, 2024

1

@Alavrgajesus
Copy link


- [ ] 

@Alavrgajesus
Copy link

Details

@Alavrgajesus
Copy link

Vv

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
4 participants