diff --git a/types/react-blessed/react-blessed-tests.tsx b/types/react-blessed/react-blessed-tests.tsx index 35ad9627044120..d7497a255577b1 100644 --- a/types/react-blessed/react-blessed-tests.tsx +++ b/types/react-blessed/react-blessed-tests.tsx @@ -211,11 +211,9 @@ const boxRef = React.createRef(); ; ; -// @ts-expect-error ; ; ; -// @ts-expect-error ; const newContextRef = React.createRef(); @@ -225,7 +223,6 @@ const newContextRef = React.createRef(); const ForwardNewContext = React.forwardRef((_props: {}, ref?: React.Ref) => ); ; -// @ts-expect-error ; const ForwardRef3 = React.forwardRef( diff --git a/types/react-redux/react-redux-tests.tsx b/types/react-redux/react-redux-tests.tsx index 321048dd6606b7..c488c0369bd359 100644 --- a/types/react-redux/react-redux-tests.tsx +++ b/types/react-redux/react-redux-tests.tsx @@ -1729,8 +1729,7 @@ function testRef() { // Should be able to pass modern refs to a ForwardRefExoticComponent const modernRef: React.Ref | undefined = undefined; ; - // Should not be able to use legacy string refs - // @ts-expect-error + // Should be able to use legacy string refs ; // ref type should agree with type of the forwarded ref // @ts-expect-error diff --git a/types/react/index.d.ts b/types/react/index.d.ts index 75367c6b35ef6a..996761f9d1df8a 100644 --- a/types/react/index.d.ts +++ b/types/react/index.d.ts @@ -189,6 +189,7 @@ declare namespace React { *
* ``` */ + // TODO: Remove the string ref special case from `PropsWithRef` once we remove LegacyRef type LegacyRef = string | Ref; /** @@ -217,9 +218,10 @@ declare namespace React { > = // need to check first if `ref` is a valid prop for ts@3.0 // otherwise it will infer `{}` instead of `never` - "ref" extends keyof ComponentPropsWithRef ? NonNullable["ref"]> extends Ref< + "ref" extends keyof ComponentPropsWithRef + ? NonNullable["ref"]> extends RefAttributes< infer Instance - > ? Instance + >["ref"] ? Instance : never : never; @@ -232,9 +234,55 @@ declare namespace React { */ type Key = string | number | bigint; + /** + * @internal The props any component can receive. + * You don't have to add this type. All components automatically accept these props. + * ```tsx + * const Component = () =>
; + * + * ``` + * + * WARNING: The implementation of a component will never have access to these attributes. + * The following example would be incorrect usage because {@link Component} would never have access to `key`: + * ```tsx + * const Component = (props: React.Attributes) => props.key; + * ``` + */ interface Attributes { key?: Key | null | undefined; } + /** + * The props any component accepting refs can receive. + * Class components, built-in browser components (e.g. `div`) and forwardRef components can receive refs and automatically accept these props. + * ```tsx + * const Component = forwardRef(() =>
); + * console.log(current)} /> + * ``` + * + * You only need this type if you manually author the types of props that need to be compatible with legacy refs. + * ```tsx + * interface Props extends React.RefAttributes {} + * declare const Component: React.FunctionComponent; + * ``` + * + * Otherwise it's simpler to directly use {@link Ref} since you can safely use the + * props type to describe to props that a consumer can pass to the component + * as well as describing the props the implementation of a component "sees". + * {@link RefAttributes} is generally not safe to describe both consumer and seen props. + * + * ```tsx + * interface Props extends { + * ref?: React.Ref | undefined; + * } + * declare const Component: React.FunctionComponent; + * ``` + * + * WARNING: The implementation of a component will not have access to the same type in versions of React supporting string refs. + * The following example would be incorrect usage because {@link Component} would never have access to a `ref` with type `string` + * ```tsx + * const Component = (props: React.RefAttributes) => props.ref; + * ``` + */ interface RefAttributes extends Attributes { /** * Allows getting a ref to the component instance. @@ -243,21 +291,13 @@ declare namespace React { * * @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs} */ - ref?: Ref | undefined; + ref?: LegacyRef | undefined; } /** * Represents the built-in attributes available to class components. */ - interface ClassAttributes extends Attributes { - /** - * Allows getting a ref to the component instance. - * Once the component unmounts, React will set `ref.current` to `null` - * (or call the ref with `null` if you passed a callback ref). - * - * @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs} - */ - ref?: LegacyRef | undefined; + interface ClassAttributes extends RefAttributes { } /** @@ -1522,6 +1562,7 @@ declare namespace React { P extends any ? ("ref" extends keyof P ? Omit : P) : P; /** Ensures that the props do not include string ref, which cannot be forwarded */ type PropsWithRef

= + // Note: String refs can be forwarded. We can't fix this bug without breaking a bunch of libraries now though. // Just "P extends { ref?: infer R }" looks sufficient, but R will infer as {} if P is {}. "ref" extends keyof P ? P extends { ref?: infer R | undefined } diff --git a/types/react/package.json b/types/react/package.json index bd194f611a50f4..956f755cc67f1a 100644 --- a/types/react/package.json +++ b/types/react/package.json @@ -180,6 +180,10 @@ { "name": "Dmitry Semigradsky", "githubUsername": "Semigradsky" + }, + { + "name": "Matt Pocock", + "githubUsername": "mattpocock" } ] } diff --git a/types/react/test/index.ts b/types/react/test/index.ts index 13c8e55db0a08a..b78f24612ab38f 100644 --- a/types/react/test/index.ts +++ b/types/react/test/index.ts @@ -446,8 +446,11 @@ const ForwardingRefComponent2 = React.forwardRef((props, ref) => { ref(e: HTMLDivElement) { if (typeof ref === "function") { ref(e); - } else if (ref) { + } else if (typeof ref === "object" && ref !== null) { ref.current = e; + } else { + // $ExpectType null + ref; } }, }); diff --git a/types/react/test/tsx.tsx b/types/react/test/tsx.tsx index 05bb1ca596265d..79aa2c9902ffe7 100644 --- a/types/react/test/tsx.tsx +++ b/types/react/test/tsx.tsx @@ -454,11 +454,9 @@ const badlyAuthoredRef: React.RefObject = { c ; ; -// @ts-expect-error ; ; ; -// @ts-expect-error ; // @ts-expect-error Undesired behavior ; @@ -482,7 +480,6 @@ const newContextRef = React.createRef(); const ForwardNewContext = React.forwardRef((_props: {}, ref?: React.Ref) => ); ; -// @ts-expect-error ; const ForwardRef3 = React.forwardRef( diff --git a/types/react/ts5.0/index.d.ts b/types/react/ts5.0/index.d.ts index b11bb2141a844b..3d3b982bf367d8 100644 --- a/types/react/ts5.0/index.d.ts +++ b/types/react/ts5.0/index.d.ts @@ -186,6 +186,7 @@ declare namespace React { *

* ``` */ + // TODO: Remove the string ref special case from `PropsWithRef` once we remove LegacyRef type LegacyRef = string | Ref; /** @@ -214,9 +215,10 @@ declare namespace React { > = // need to check first if `ref` is a valid prop for ts@3.0 // otherwise it will infer `{}` instead of `never` - "ref" extends keyof ComponentPropsWithRef ? NonNullable["ref"]> extends Ref< + "ref" extends keyof ComponentPropsWithRef + ? NonNullable["ref"]> extends RefAttributes< infer Instance - > ? Instance + >["ref"] ? Instance : never : never; @@ -230,30 +232,66 @@ declare namespace React { type Key = string | number | bigint; /** - * @internal You shouldn't need to use this type since you never see these attributes - * inside your component or have to validate them. + * @internal The props any component can receive. + * You don't have to add this type. All components automatically accept these props. + * ```tsx + * const Component = () =>
; + * + * ``` + * + * WARNING: The implementation of a component will never have access to these attributes. + * The following example would be incorrect usage because {@link Component} would never have access to `key`: + * ```tsx + * const Component = (props: React.Attributes) => props.key; + * ``` */ interface Attributes { key?: Key | null | undefined; } + /** + * The props any component accepting refs can receive. + * Class components, built-in browser components (e.g. `div`) and forwardRef components can receive refs and automatically accept these props. + * ```tsx + * const Component = forwardRef(() =>
); + * console.log(current)} /> + * ``` + * + * You only need this type if you manually author the types of props that need to be compatible with legacy refs. + * ```tsx + * interface Props extends React.RefAttributes {} + * declare const Component: React.FunctionComponent; + * ``` + * + * Otherwise it's simpler to directly use {@link Ref} since you can safely use the + * props type to describe to props that a consumer can pass to the component + * as well as describing the props the implementation of a component "sees". + * {@link RefAttributes} is generally not safe to describe both consumer and seen props. + * + * ```tsx + * interface Props extends { + * ref?: React.Ref | undefined; + * } + * declare const Component: React.FunctionComponent; + * ``` + * + * WARNING: The implementation of a component will not have access to the same type in versions of React supporting string refs. + * The following example would be incorrect usage because {@link Component} would never have access to a `ref` with type `string` + * ```tsx + * const Component = (props: React.RefAttributes) => props.ref; + * ``` + */ interface RefAttributes extends Attributes { /** * Allows getting a ref to the component instance. * Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref). * @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom} */ - ref?: Ref | undefined; + ref?: LegacyRef | undefined; } /** * Represents the built-in attributes available to class components. */ - interface ClassAttributes extends Attributes { - /** - * Allows getting a ref to the component instance. - * Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref). - * @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom} - */ - ref?: LegacyRef | undefined; + interface ClassAttributes extends RefAttributes { } /** @@ -1520,6 +1558,7 @@ declare namespace React { P extends any ? ("ref" extends keyof P ? Omit : P) : P; /** Ensures that the props do not include string ref, which cannot be forwarded */ type PropsWithRef

= + // Note: String refs can be forwarded. We can't fix this bug without breaking a bunch of libraries now though. // Just "P extends { ref?: infer R }" looks sufficient, but R will infer as {} if P is {}. "ref" extends keyof P ? P extends { ref?: infer R | undefined } diff --git a/types/react/ts5.0/test/index.ts b/types/react/ts5.0/test/index.ts index 6c636652013f18..08c0101dc0e741 100644 --- a/types/react/ts5.0/test/index.ts +++ b/types/react/ts5.0/test/index.ts @@ -443,8 +443,11 @@ const ForwardingRefComponent2 = React.forwardRef((props, ref) => { ref(e: HTMLDivElement) { if (typeof ref === "function") { ref(e); - } else if (ref) { + } else if (typeof ref === "object" && ref !== null) { ref.current = e; + } else { + // $ExpectType null + ref; } }, }); diff --git a/types/react/ts5.0/test/tsx.tsx b/types/react/ts5.0/test/tsx.tsx index 9184414b38bade..f1a6ac235bd369 100644 --- a/types/react/ts5.0/test/tsx.tsx +++ b/types/react/ts5.0/test/tsx.tsx @@ -454,11 +454,9 @@ const badlyAuthoredRef: React.RefObject = { c ; ; -// @ts-expect-error ; ; ; -// @ts-expect-error ; // @ts-expect-error Undesired behavior ; @@ -482,7 +480,6 @@ const newContextRef = React.createRef(); const ForwardNewContext = React.forwardRef((_props: {}, ref?: React.Ref) => ); ; -// @ts-expect-error ; const ForwardRef3 = React.forwardRef(