diff --git a/.changeset/polymorphic-prop-inference.md b/.changeset/polymorphic-prop-inference.md
new file mode 100644
index 00000000000..9b3b269e8fa
--- /dev/null
+++ b/.changeset/polymorphic-prop-inference.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': patch
+---
+
+Polymorphic components: Fix TypeScript inference for component-specific props while preserving `as` support.
diff --git a/packages/react/src/ActionList/ActionList.types.test.tsx b/packages/react/src/ActionList/ActionList.types.test.tsx
new file mode 100644
index 00000000000..c572e297165
--- /dev/null
+++ b/packages/react/src/ActionList/ActionList.types.test.tsx
@@ -0,0 +1,46 @@
+import React from 'react'
+import {ActionList} from './index'
+
+export function shouldAcceptLinkItemAs() {
+ return (
+ ()}
+ onClick={event => {
+ type Event = Expect>>
+ }}
+ >
+ Action
+
+ )
+}
+
+const CustomLink = React.forwardRef(
+ ({to, children}, ref) => {
+ return (
+
+ {children}
+
+ )
+ },
+)
+
+export function shouldAcceptCustomLinkItemAs() {
+ return ()} />
+}
+
+export function shouldRequireCustomLinkItemProps() {
+ // @ts-expect-error the custom component requires to
+ return
+}
+
+export function shouldAcceptListAs() {
+ return ()} />
+}
+
+type Expect = T
+type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false
diff --git a/packages/react/src/ActionList/LinkItem.tsx b/packages/react/src/ActionList/LinkItem.tsx
index 02cd952ea34..88c03422677 100644
--- a/packages/react/src/ActionList/LinkItem.tsx
+++ b/packages/react/src/ActionList/LinkItem.tsx
@@ -38,7 +38,7 @@ const LinkItemComponent = fixedForwardRef(
forwardedRef: ForwardedRef,
) => {
return (
- -
className={className}
active={active}
inactiveText={inactiveText}
diff --git a/packages/react/src/ActionList/List.tsx b/packages/react/src/ActionList/List.tsx
index 0abeb7e4722..11c00a37db7 100644
--- a/packages/react/src/ActionList/List.tsx
+++ b/packages/react/src/ActionList/List.tsx
@@ -109,7 +109,6 @@ const UnwrappedList = (
return (
{slots.heading}
- {/* @ts-expect-error ref needs a non nullable ref */}
}
+
+export function shouldAcceptButtonProps() {
+ return (
+ ()}
+ onClick={event => {
+ type Event = Expect>>
+ }}
+ />
+ )
+}
+
+const CustomLink = React.forwardRef(
+ ({to, children}, ref) => {
+ return (
+
+ {children}
+
+ )
+ },
+)
+
+export function shouldAcceptCustomComponentProps() {
+ return ()} />
+}
+
+export function shouldRequireCustomComponentProps() {
+ // @ts-expect-error the custom component requires to
+ return
+}
+
+export function shouldRejectIncorrectRef() {
+ // @ts-expect-error a button requires a button ref
+ return ()} />
+}
+
+type Expect = T
+type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false
diff --git a/packages/react/src/utils/modern-polymorphic.ts b/packages/react/src/utils/modern-polymorphic.ts
index 62ca5ea264b..2044ffe5ba1 100644
--- a/packages/react/src/utils/modern-polymorphic.ts
+++ b/packages/react/src/utils/modern-polymorphic.ts
@@ -25,11 +25,13 @@ export const fixedForwardRef = forwardRef as FixedForwardRef
/**
* Simplified polymorphic props type that handles the common pattern of
* `DistributiveOmit, 'as'>`
+ * Keep own props separate so their types remain available in generic components.
*/
export type PolymorphicProps<
TAs extends ElementType,
TDefaultElement extends ElementType = 'div',
Props = object,
-> = DistributiveOmit & Props, 'as'> & {
- as?: TAs
-}
+> = DistributiveOmit, 'as'> &
+ DistributiveOmit & {
+ as?: TAs
+ }
diff --git a/packages/react/src/utils/modern-polymorphic.types.test.tsx b/packages/react/src/utils/modern-polymorphic.types.test.tsx
new file mode 100644
index 00000000000..fbaffefade3
--- /dev/null
+++ b/packages/react/src/utils/modern-polymorphic.types.test.tsx
@@ -0,0 +1,55 @@
+import React from 'react'
+import type {PolymorphicProps} from './modern-polymorphic'
+
+export function shouldPreserveOwnPropTypes(
+ props: PolymorphicProps<
+ As,
+ 'div',
+ {
+ delayMs?: number
+ politeness?: 'assertive' | 'polite'
+ title: string
+ }
+ >,
+) {
+ const {delayMs, politeness, title} = props
+
+ type Delay = Expect>
+ type Politeness = Expect>
+ type Title = Expect>
+}
+
+export function shouldUsePolymorphicAsInsteadOfOwnAs() {
+ const props: PolymorphicProps<'button', 'a', {as?: 'a'; muted?: boolean}> = {
+ as: 'button',
+ muted: true,
+ disabled: true,
+ ref: React.createRef(),
+ onClick: event => {
+ type Event = Expect>>
+ },
+ }
+ return props
+}
+
+export function shouldPreserveIntersectingPropTypes() {
+ const props: PolymorphicProps<'input', 'div', {size?: 'small' | 'large'}> = {
+ as: 'input',
+ // @ts-expect-error own props do not override conflicting element props
+ size: 'small',
+ }
+ return props
+}
+
+export function shouldPreserveOwnPropUnions(
+ props: PolymorphicProps<'div', 'div', {kind: 'text'; text: string} | {kind: 'count'; count: number}>,
+) {
+ if (props.kind === 'text') {
+ type Text = Expect>
+ } else {
+ type Count = Expect>
+ }
+}
+
+type Expect = T
+type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false