Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions src/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ type ButtonOwnProps<TElement extends React.ElementType = 'button'> = {

type ButtonProps<TElement extends React.ElementType = 'button'> =
ButtonOwnProps<TElement> &
Omit<
React.ComponentPropsWithoutRef<TElement>,
keyof ButtonOwnProps<TElement>
>
Omit<React.ComponentPropsWithRef<TElement>, keyof ButtonOwnProps<TElement>>

type ButtonComponent = <TElement extends React.ElementType = 'button'>(
props: ButtonProps<TElement>,
Expand Down Expand Up @@ -102,16 +99,22 @@ function getDefaultRounded(size: ButtonSize): ButtonRounded {
return 'lg'
}

export const Button: ButtonComponent = ({
as,
children,
variant = 'primary',
color = 'blue',
size,
rounded,
className,
...props
}) => {
type ButtonInnerProps = ButtonOwnProps & Record<string, unknown>

export const Button: ButtonComponent = React.forwardRef<
HTMLElement,
ButtonInnerProps
>(function Button(props, ref) {
Comment on lines +102 to +107
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

In React 19 + TypeScript, what is the recommended way to type a polymorphic component with an asprop and forwarded refs without usingas unknown as casts?

💡 Result:

In React 19, the ref prop is now treated as a standard prop, which simplifies the implementation of polymorphic components by removing the need for forwardRef [1][2][3]. To create a type-safe polymorphic component without using as unknown as casts, you should define a generic component that accepts an as prop and destructures ref directly from the props object [2][3]. The recommended approach involves: 1. Defining a generic type for your component props that extends React.ElementType [4][5]. 2. Using React.ComponentPropsWithRef<C> to ensure the component correctly inherits the props and ref type of the element specified by the as prop [6][4]. 3. Destructuring ref and as from the props and passing them directly to the underlying element [2]. Example Pattern: type PolymorphicProps<C extends React.ElementType, Props = {}> = Props & { as?: C } & Omit<React.ComponentPropsWithRef, keyof Props | 'as'>; export const PolymorphicComponent = ({ as, ref,...props }: PolymorphicProps<C, { children?: React.ReactNode }>) => { const Component = as || 'div'; return <Component ref={ref} {...props} />; }; By destructuring ref as a standard prop, you avoid the complex type signatures previously required by forwardRef [2][3]. This pattern allows TypeScript to correctly infer the element type and its associated ref, maintaining full type safety without requiring type assertions [4]. If you are working in a codebase that still requires forwardRef for compatibility, you can continue to use it, but it is no longer necessary for new components in React 19 [1][2].

Citations:


🏁 Script executed:

# Check React version
fd -e json -e ts -e js package.json tsconfig.json | head -5

Repository: TanStack/tanstack.com

Length of output: 166


🏁 Script executed:

# Read the Button.tsx file to see full context
cat -n src/ui/Button.tsx | sed -n '95,150p'

Repository: TanStack/tanstack.com

Length of output: 1636


🏁 Script executed:

# Check if there are type definitions for ButtonProps and ButtonComponent
rg -A 5 "type ButtonComponent|type ButtonProps|interface ButtonProps" src/ui/Button.tsx

Repository: TanStack/tanstack.com

Length of output: 493


🏁 Script executed:

# Check React version in package.json
rg "react" package.json -A 1 -B 1 | head -20

Repository: TanStack/tanstack.com

Length of output: 764


🏁 Script executed:

# Check if ButtonOwnProps is properly generic
rg -B 5 "type ButtonOwnProps" src/ui/Button.tsx

Repository: TanStack/tanstack.com

Length of output: 258


🏁 Script executed:

# Check ButtonInnerProps usage and ButtonOwnProps scope
rg "ButtonOwnProps" src/ui/Button.tsx

Repository: TanStack/tanstack.com

Length of output: 366


🏁 Script executed:

# Find React version more explicitly
cat package.json | jq '.dependencies.react, .devDependencies.react' 2>/dev/null || rg '"react":\s*"' package.json

Repository: TanStack/tanstack.com

Length of output: 79


🏁 Script executed:

# Get full ButtonOwnProps and ButtonProps definitions
rg -B 2 -A 15 "type ButtonOwnProps" src/ui/Button.tsx

Repository: TanStack/tanstack.com

Length of output: 630


🏁 Script executed:

# Check if the component is being used polymorphically in the codebase
rg "Button\s+as=" --type tsx --type ts | head -10

Repository: TanStack/tanstack.com

Length of output: 93


Update to use React 19's simpler polymorphic pattern without forwardRef.

The component has a polymorphic type contract (ButtonComponent is generic over TElement) but the implementation uses monomorphic forwardRef<HTMLElement, ButtonInnerProps>, creating a type mismatch that requires unsafe casts (line 117: props as ..., line 145: as unknown as ButtonComponent). Since the codebase is on React 19.2.3, ref can be treated as a standard prop, eliminating the need for forwardRef entirely and removing the type coupling issue.

♻️ Suggested direction
-type ButtonInnerProps = ButtonOwnProps & Record<string, unknown>

-export const Button: ButtonComponent = React.forwardRef<
-  HTMLElement,
-  ButtonInnerProps
->(function Button(props, ref) {
+const ButtonImpl = <TElement extends React.ElementType = 'button'>(
+  props: ButtonProps<TElement> & { ref?: React.ForwardedRef<any> }
+) => {
+  const { ref, as, children, variant = 'primary', color = 'blue', size, rounded, className, ...rest } = props
-  const {
-    as,
-    children,
-    variant = 'primary',
-    color = 'blue',
-    size,
-    rounded,
-    className,
-    ...rest
-  } = props as ButtonOwnProps & Record<string, unknown>
   const Component = as || 'button'
   const resolvedSize = size ?? getDefaultSize(variant)
   const resolvedRounded = rounded ?? getDefaultRounded(resolvedSize)

   const colorStyles =
     variant === 'primary'
       ? primaryColorStyles[color]
       : variant === 'icon'
         ? iconColorStyles[color]
         : ''

   return React.createElement(
     Component,
     {
       ref,
       className: twMerge(
         baseStyles,
         variantStyles[variant],
         sizeStyles[resolvedSize],
         roundedStyles[resolvedRounded],
         colorStyles,
         className,
       ),
       ...rest,
     },
     children,
   )
-}) as unknown as ButtonComponent
+}
+
+export const Button = ButtonImpl as ButtonComponent

Also applies to: 117-117, 145-145

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/Button.tsx` around lines 102 - 107, The implementation uses
React.forwardRef and a monomorphic ref type causing unsafe casts; change Button
to the React 19 polymorphic pattern by removing React.forwardRef and making
Button a generic function component that accepts ref as a normal prop. Update
ButtonInnerProps (and any type alias used by ButtonComponent) to be generic over
TElement so the component signature matches ButtonComponent, accept an optional
ref prop (e.g., ref?: React.Ref<TElement>) in the props, and remove the unsafe
casts currently applied to props and to the exported component (the "props as
..." and "as unknown as ButtonComponent" usages). Ensure the exported Button has
the generic polymorphic type (ButtonComponent) without using forwardRef.

const {
as,
children,
variant = 'primary',
color = 'blue',
size,
rounded,
className,
...rest
} = props as ButtonOwnProps & Record<string, unknown>
const Component = as || 'button'
const resolvedSize = size ?? getDefaultSize(variant)
const resolvedRounded = rounded ?? getDefaultRounded(resolvedSize)
Expand All @@ -126,6 +129,7 @@ export const Button: ButtonComponent = ({
return React.createElement(
Component,
{
ref,
className: twMerge(
baseStyles,
variantStyles[variant],
Expand All @@ -134,8 +138,8 @@ export const Button: ButtonComponent = ({
colorStyles,
className,
),
...props,
...rest,
},
children,
)
}
}) as unknown as ButtonComponent
Loading