Skip to content

Fix remove-forward-ref TypeScript output for React 19#348

Open
adamjcooper wants to merge 1 commit into
reactjs:masterfrom
adamjcooper:fix-remove-forward-ref-types
Open

Fix remove-forward-ref TypeScript output for React 19#348
adamjcooper wants to merge 1 commit into
reactjs:masterfrom
adamjcooper:fix-remove-forward-ref-types

Conversation

@adamjcooper

@adamjcooper adamjcooper commented Jul 7, 2026

Copy link
Copy Markdown

Fixes #336

Running this codemod before it merges

jscodeshift can run the transform file straight from a checkout of this branch:

git clone -b fix-remove-forward-ref-types https://github.com/adamjcooper/react-codemod.git /tmp/react-codemod

npx jscodeshift@latest \
  -t /tmp/react-codemod/transforms/remove-forward-ref.ts \
  --parser=tsx \
  --extensions=tsx,ts \
  path/to/your/src

--parser=tsx is required (the transform doesn't export a parser). Add jsx,js to --extensions if you have untyped files, use --dry --print to preview, and run on a clean git tree so the changes are easy to review or discard.

Problem

On TypeScript code, remove-forward-ref produces output with type errors:

  1. The emitted ref prop is required and RefObject-only (ref: React.RefObject<T>). A forwardRef component's ref was optional and could also be a callback ref, so call sites that pass no ref — or a callback ref — no longer typecheck. This is the error reported in Refs required by react/19/remove-forward-ref #336.
  2. Union and intersection types are dropped. Only bare type references and type literals were carried over; any other props/ref type produced an untyped destructure, making every prop an implicit any and breaking all consumers.
  3. Typed ref parameters are only recognized as React.ForwardedRef<SingleReference>. Bare named imports (ForwardedRef<T>) and union element types fell back to unknown.

Fix

  • Emit ref?: React.Ref<T> — optional, and accepting ref objects, callback refs, and null, matching the ref prop of ForwardRefExoticComponent.
  • Accept any TS type for props and ref, parenthesizing unions and conditional types inside the emitted intersection: (A | B) & { ref?: React.Ref<T> }.
  • Recognize bare named-import ref annotations (ForwardedRef<T>, Ref<T>, …) and union element types.

Example:

// before
const Tab = forwardRef<HTMLButtonElement | HTMLAnchorElement, PropsWithChildren<Props>>(
  (props, ref) => { /* ... */ }
);

// old output — annotation dropped, all props become `any`
const Tab = ({ ref, ...props }) => { /* ... */ };

// new output
const Tab = (
  {
    ref,
    ...props
  }: PropsWithChildren<Props> & {
    ref?: React.Ref<HTMLButtonElement | HTMLAnchorElement>
  }
) => { /* ... */ };

Relationship to #338 and #339

@iamakulov's PRs address parts of this issue, and this PR covers both (apologies for the overlap — happy to coordinate however is easiest to review):

Testing

  • 4 updated + 6 new fixtures: union refs, union props, union-of-intersections, intersection props, bare ForwardedRef import, and a union inside the ref argument's annotation. All 17 tests pass; lint is clean.
  • Validated on a real ~90-file production migration that previously required ~500 manual type-error fixes after running this codemod: the transform now reproduces the hand-written annotations exactly. The only remaining manual work was app code whose own signatures mention ForwardedRef (e.g. custom useForwardedRef hooks), which a call-site transform can't know about.

Notes for reviewers

  • The emitted React.Ref<T> assumes the React namespace is in scope — same as the previous React.RefObject<T> output, so not a regression.
  • Type imports left unused by the transform (e.g. ForwardedRef) are not pruned, as before; the ref-arg-named-import fixture documents this.

🤖 Generated with Claude Code, then manually reviewed and edited.

The transform had three problems with the ref prop type it emitted:

1. The ref prop was typed as `ref: React.RefObject<T>` -- required and
   RefObject-only. A forwardRef component accepts an optional ref that
   may be a ref object, a callback ref, or null, so every call site
   that didn't pass a ref (or passed a callback ref) became a type
   error. Emit `ref?: React.Ref<T>` instead, matching the type that
   ForwardRefExoticComponent exposed.

2. Props and ref types were only carried over when they were a simple
   type reference or type literal. Union types, intersections, and
   other type expressions (e.g. `forwardRef<HTMLButtonElement |
   HTMLAnchorElement, PropsWithChildren<Props>>`) caused the annotation
   to be dropped entirely, leaving an untyped destructure where every
   prop was inferred as any. Accept any TS type, parenthesizing unions
   and conditional types when placed in the emitted intersection.

3. The ref argument's annotation was only recognized in the qualified
   `React.ForwardedRef<SingleReference>` form. Recognize bare named
   imports (`ForwardedRef<T>`, `Ref<T>`, etc.) and non-reference
   element types such as unions.

Fixes reactjs#336

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@adamjcooper adamjcooper force-pushed the fix-remove-forward-ref-types branch from de9484b to 3879a65 Compare July 7, 2026 17:13
@adamjcooper adamjcooper marked this pull request as ready for review July 7, 2026 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refs required by react/19/remove-forward-ref

1 participant