microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.94k stars 12.48k forks source link

React's ComponentProps type issues in TypeScript 5.6.2 #59937

Open jakubmazanec opened 1 month ago

jakubmazanec commented 1 month ago

🔎 Search Terms

react, forwardRef, component props, ComponentProps

🕗 Version & Regression Information

⏯ 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 commented 1 month ago

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 commented 1 month ago

A reasonable self-isolated repro: TS playground

jakubmazanec commented 1 month ago

Huh, interesting. Thank you for the investigation.

Alavrgajesus commented 1 month ago

`` ceb4951646b978c517240e7bf43517a53d969c25

RyanCavanaugh commented 1 month ago

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

Andarist commented 1 month ago

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: https://github.com/microsoft/TypeScript/pull/59972 :)

jakubmazanec commented 1 month ago

Great job as usual, thank you @Andarist!

Alavrgajesus commented 1 month ago

1

Alavrgajesus commented 1 month ago

- [ ] 
Alavrgajesus commented 1 month ago
Details

Alavrgajesus commented 1 month ago

Vv