Skip to content

Components: Update polymorphism types to allow all attributes when specifying as - #80705

Merged
aduth merged 7 commits into
trunkfrom
update/components-polymorphism-flat-attributes
Jul 30, 2026
Merged

Components: Update polymorphism types to allow all attributes when specifying as#80705
aduth merged 7 commits into
trunkfrom
update/components-polymorphism-flat-attributes

Conversation

@aduth

@aduth aduth commented Jul 24, 2026

Copy link
Copy Markdown
Member

What?

Updates the polymorphism types used in @wordpress/components to expose all possible HTML attributes when as is specified, rather than narrowing the types based on the tagName provided to as.

The performance impact on TypeScript builds is significant, reducing memory usage by -47% (7.4GB to 4.0GB), TypeScript build time by -49% (13.7s to 7.0s), and internal instantiations by -54% (42.5M to 19.5M).

Why?

How?

As described in #80655 , these polymorphism types are a major contributor to overall TypeScript build times and memory consumption. While #80655 tried to fully maintain the original intent of the polymorphism types, the changes here propose a more drastic change: If as is provided, all valid HTML attributes are accessible on that component. This means someone could do something like <VisuallyHidden as="div" htmlFor="id" /> and TypeScript would consider it valid, which was not the case before now, as htmlFor is not a valid attribute for div elements.

This is meant to be a reasonable compromise given the circumstances, and how new development in @wordpress/ui leans on render props instead of as polymorphism. render props don't suffer from this problem because props which are relevant to the component passed to render are applied to the element directly.

This isn't all bad though, since:

  • Those additional HTML attributes only become known if as is provided, and otherwise the component's own props are the only ones available.
  • The attribute types are still accurately typed, so TypeScript validates that htmlFor is a string, for example.

Testing Instructions

Verify type-checking passes:

npm run build

For bonus points, try some of the scenarios described under "How", like how you can't assign htmlFor unless specifying as, or that htmlFor can be used with as="div", or how htmlFor can't be assigned to a type other than string.

Also check performance for yourself using snippets from #80364 "Testing Instructions":

If you want to validate performance yourself, you can compare the following set of commands on this branch and trunk:

rm -f packages/components/tsconfig.tsbuildinfo
/usr/bin/time -l npx tsc -p packages/components/tsconfig.json --extendedDiagnostics 2>&1 \
  | grep -E '^(Symbols|Instantiations|Memory used|Memory allocs):|maximum resident'

Or full project timings and memory usage:

npx tsc --build --clean >/dev/null 2>&1
rm -rf packages/*/build-types
/usr/bin/time -l npx tsc --build 2>&1 | grep -E 'maximum resident|real'

Use of AI Tools

Used Cursor IDE + Auto (likely Composer) model to research and implement, with manual iterations, particularly on the code comments.

@aduth aduth added the [Type] Build Tooling Issues or PRs related to build tooling label Jul 24, 2026
@aduth
aduth requested review from a team and ajitbohra as code owners July 24, 2026 20:02
@github-actions github-actions Bot added the [Package] Components /packages/components label Jul 24, 2026
@aduth
aduth requested a review from manzoorwanijk July 24, 2026 20:03
@aduth
aduth requested a review from Mamaduka July 24, 2026 20:04
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

Size Change: -7 B (0%)

Total Size: 7.76 MB

📦 View Changed
Filename Size Change
build/scripts/preferences/index.min.js 3.3 kB -7 B (-0.21%)

compressed-size-action

Comment on lines +13 to +16
/**
* The content to render inside the wrapper.
*/
children?: ReactNode;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

From what I understand, this should have always been here, and the previous polymorphism types incorrectly typed children for non-polymorphic components. Generally it should be expected that components define their own children types, which we're more consistent about in @wordpress/ui (e.g. ComponentProps explicitly omits children)

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: aduth <aduth@git.wordpress.org>
Co-authored-by: manzoorwanijk <manzoorwanijk@git.wordpress.org>
Co-authored-by: mirka <0mirka00@git.wordpress.org>
Co-authored-by: tyxla <tyxla@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@aduth

aduth commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Oof, type-checking failures in CI. Guessing some local cache might have misled me locally. I'll throw this back to draft temporarily and revisit it on Monday, unless someone wants to go ahead and carry the torch on this one.

Edit: Nevermind, decided to tackle it now.

@aduth
aduth marked this pull request as draft July 24, 2026 20:08
@github-actions github-actions Bot added the [Package] Preferences /packages/preferences label Jul 24, 2026
path={ `/${ tab.name }` }
// @ts-expect-error: Navigator.Button is currently typed in a way that prevents Item from being passed in
as={ Item }
isAction

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is safe:

- Removed `isAction` prop from `Item`. The component will now rely on `onClick` to render as a `button` ([35152](https://github.com/WordPress/gutenberg/pull/35152)).

Navigator.Button provides onClick:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The reason the removal has to happen is because the TypeScript error on the line above assigning as={ Item } no longer errors, and the suppression through @ts-expect-error was subsequently not checking this prop, which had been removed from the types in #35152. Once the props were checked, it flagged the prop as not part of the Item props types.

@aduth
aduth marked this pull request as ready for review July 24, 2026 20:32
@aduth
aduth requested a review from talldan as a code owner July 24, 2026 20:32
@github-actions

Copy link
Copy Markdown

Flaky tests detected in 7e5d36a.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/30124455510
📝 Reported issues:

@tyxla

tyxla commented Jul 27, 2026

Copy link
Copy Markdown
Member

Wow, nice work @aduth. This will likely greatly improve the performance of extenders of @wordpress/components that historically have suffered from slow builds or even OOMs when extending @wordpress/components types more aggressively.

@manzoorwanijk manzoorwanijk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you for improving the performance here. I have some inline questions/suggestions.

Also, I was wondering whether we should have some type tests for the usages.

* Compatible with Emotion/`styled` `as` props and React's `ElementType`, without
* using the default `React.ElementType` mapped type helper directly.
*/
type PolymorphicAs =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this be named something that explains what it actually is rather than what it's used for? Like PolymorphicElement, PolymorphicElementType etc.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Sure, that does seem better 👍

Also worth noting that this separate type should ideally be temporary, as there's a lot of active work underway to eliminate Emotion altogether (#66806), at which point we won't need this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

at which point we won't need this

I think we do? Unless we get rid of the as prop back compat. (We're removing Emotion, but keeping the as prop for back compat.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I dug into this a bit more. I understood that we would continue supporting as, but it was my impression from rounds of iteration with my AI agent that there was something specific about Emotion's typings that made it necessary to type it this way specifically. But on closer examination, I think my agent made an inappropriate leap based on trying to simplify this typing to something like as?: string | React.JSXElementConstructor< any > as the ideal end-case, and the only thing holding us up was Emotion's stricter treatment of keyof React.JSX.IntrinsicElements vs. string. I think this isn't so much an "Emotion" thing as it is just better more accurate types. It's a little bit of a trade-off because the simplicity of string could have better performance, and I'd be curious to test the impact once we do get rid of Emotion to see if it's worth it, but keyof React.JSX.IntrinsicElements means that we still validate that as="label" is a valid tag name.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is improved in 85789d2 . I opted to just inline the type to solve both problems at once. Details in the extended commit description:

Improves a couple things:

  • Avoids a named type that references what it's used for (the "as" prop) rather than what it is
  • Avoids comments specifically referencing Emotion as the reason it's typed the way it is, as this is an objectively better, more accurate typing

Comment thread packages/components/src/context/wordpress-component.ts Outdated
Comment thread packages/components/src/toggle-group-control/types.ts
/**
* Size of the underlying Modal. See `Modal`'s `size` prop.
*/
size?: ModalProps[ 'size' ];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This prop didn't land in the README for the component.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The short answer is that the component lacks a docs-manifest.json which would opt it into automatic documentation, and so its README is maintained manually. We could update it manually, but I'm also curious to dig into why it doesn't have a manifest 🙂

@aduth aduth Jul 29, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I tried adding the docs-manifest.json and it works (also brings other missing props like isBusy), but it causes a lot of unrelated changes to the README.md (changes formatting quite a bit) that I'd suggest we tackle separately. In the interim, da70077 adds the manual prop documentation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Separate pull request for the auto-documentation: #80866

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We could update it manually, but I'm also curious to dig into why it doesn't have a manifest 🙂

I set up the readme autogen system pretty late in the game, so a lot of them simply aren't migrated yet. Nothing more 😄

aduth added 7 commits July 29, 2026 16:14
Improves a couple things:

- Avoids a named type that references what it's used for (the "as" prop) rather than what it is
- Avoids comments specifically referencing Emotion as the reason it's typed the way it is, as this is an objectively better, more accurate typing
See: #80705 (comment)

Since this can cascade to inner types like event handler props, we can do better than the loose `any` type as we know this will be an element.
@aduth
aduth force-pushed the update/components-polymorphism-flat-attributes branch from 7e5d36a to da70077 Compare July 29, 2026 20:14

@manzoorwanijk manzoorwanijk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good to me now. Thank you for working on this.

@aduth

aduth commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

As a more visible data point, this looks to bring the typical runtime of "Type checking" in CI from about 2m40s to 1m45s (-35%).

Five examples before, from the latest 5 commits on trunk:

Two examples after, from the latest commit here and the one before my last force-push:

@aduth
aduth merged commit b7376ac into trunk Jul 30, 2026
65 of 71 checks passed
@aduth
aduth deleted the update/components-polymorphism-flat-attributes branch July 30, 2026 15:09
@github-actions github-actions Bot added this to the Gutenberg 23.8 milestone Jul 30, 2026
peterwilsoncc pushed a commit to peterwilsoncc/gutenberg-build that referenced this pull request Jul 30, 2026
…ecifying `as` (#80705)

* Components: Update polymorphism types to allow all attributes for all components

* Components: Restore polymorphism for components passed as `as`

* Sync components documentation

* Update CHANGELOGs

* Components: Inline PolymorphicAs typings

Improves a couple things:

- Avoids a named type that references what it's used for (the "as" prop) rather than what it is
- Avoids comments specifically referencing Emotion as the reason it's typed the way it is, as this is an objectively better, more accurate typing

* Components: Narrow generic type on polymorphic intrinsic types

See: WordPress/gutenberg#80705 (comment)

Since this can cascade to inner types like event handler props, we can do better than the loose `any` type as we know this will be an element.

* Components: Document size prop

Co-authored-by: aduth <aduth@git.wordpress.org>
Co-authored-by: manzoorwanijk <manzoorwanijk@git.wordpress.org>
Co-authored-by: mirka <0mirka00@git.wordpress.org>
Co-authored-by: tyxla <tyxla@git.wordpress.org>

Source: WordPress/gutenberg@b7376ac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Components /packages/components [Package] Preferences /packages/preferences [Type] Build Tooling Issues or PRs related to build tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants