Skip to content

fix: published types silently resolve React to any without @types/react - #2046

Merged
HichamELBSI merged 1 commit into
strapi:mainfrom
unrevised6419:fix/missing-types-react-peer-dependency
Sep 24, 2026
Merged

HichamELBSI merged 1 commit into
strapi:mainfrom
unrevised6419:fix/missing-types-react-peer-dependency

Conversation

@unrevised6419

Copy link
Copy Markdown
Contributor

What does it do?

Declares @types/react as an optional peer dependency of the three packages whose published type declarations import React types: @strapi/design-system, @strapi/icons and @strapi/ui-primitives.

"peerDependencies": {
  "@types/react": "^18.0.0",
  "react": "^18.0.0",
  ...
},
"peerDependenciesMeta": {
  "@types/react": { "optional": true }
}

Manifest-only. No source, build or runtime change.

Why is it needed?

This does not fail loudly at an import — it silently corrupts the inferred shape of the library's public API.

The published declarations import React types directly, e.g. dist/primitives/Box/Box.d.ts:

import { CSSProperties } from 'styled-components';
import { ResponsiveProperty, ResponsiveThemeProperty } from '../../helpers/handleResponsiveValues';
import { PolymorphicComponentPropsWithRef } from '../../types';
import * as React from 'react';

86 declaration files in @strapi/design-system, 214 in @strapi/icons, and 4 in @strapi/ui-primitives import from react. But @types/react appears in neither dependencies nor peerDependencies of any of them. The react package ships no types of its own, so a consumer's TypeScript has to locate @types/react on its own — via ambient node_modules layout that these packages neither declare nor control. (The styled-components import on line 1 is fine; that package ships its own types.)

When that ambient walk fails, resolution lands on the untyped runtime entry instead:

======== Resolving module 'react' from '.../links/@strapi/design-system/2.2.3/<hash>/node_modules/@strapi/design-system/dist/primitives/Box/Box.d.ts'. ========
File '.../links/@strapi/design-system/2.2.3/<hash>/node_modules/react/index.js' exists - use it as a name resolution result.

react/index.js has no types, so React.* degrades to any. That propagates through the polymorphic prop helpers — and Pick over any loses optionality, because Pick<any, 'tag'> is { tag: any }: required, not optional.

So dist/components/SubNav/SubNavHeader.d.ts:

export interface SubNavHeaderProps extends Pick<TypographyProps<'h2'>, 'tag'>, Partial<Pick<SearchbarProps, 'onClear' | 'onChange' | 'onSubmit' | 'placeholder'>> {
    id?: string;
    label: string;
    ...
}

turns a genuinely optional tag into a required one, and correct consumer code stops compiling:

admin/src/components/LeftMenu.tsx(32,5): error TS2741: Property 'tag' is missing in type '{ label: string; }' but required in type 'SubNavHeaderProps'.

<SubNavHeader label={...} /> is valid usage per the design system's own docs. The error names tag, which sends a consumer hunting through SubNav for a bug that isn't there — the actual cause is three layers away in an undeclared types dependency. Every Pick/Omit-derived prop type in the library is exposed to this same distortion, so the visible symptom differs per consumer, which makes it that much harder to trace back here.

Declaring the peer dependency fixes it because it makes the package manager place @types/react on the package's own resolution path, rather than leaving it to whatever the ambient layout happens to be.

This is forward-looking, not one consumer's edge case

The failure was reproduced in a pnpm 11.21.0 monorepo with enableGlobalVirtualStore: true. Under that layout packages live in a global store outside the consuming project, so TypeScript walking up from a published .d.ts never reaches the consumer's node_modules. Both 2.2.3 and 2.2.4 reproduce.

pnpm has stated it intends to make the global virtual store the default. At that point every TypeScript consumer of the design system hits this, not only those who opted in early.

Two details a reviewer will ask about

  • optional: true is required, not cosmetic. JavaScript-only consumers have no @types/react. Under strictPeerDependencies (or npm's peer auto-install) a non-optional peer would break their install.
  • The range mirrors each package's own react peer range, exactly. All three packages declare react: "^18.0.0", so the types peer is ^18.0.0 — no wider, no narrower. The types version must track the React version the consumer actually runs; a wider range (e.g. "*") would let a consumer on React 18 satisfy the peer with @types/react@19 and get wrong types with no warning, and a narrower or different range would fight the consumer's own valid choice. When these packages widen their react peer to include React 19, the @types/react peer should widen with it.

This is the established pattern in the ecosystem for React libraries that ship types — both of these mirror their own react peer as an optional peer:

  • @testing-library/react@16.3.1 → "@types/react": "^18.0.0 || ^19.0.0", optional
  • zustand@5.0.9 → "@types/react": ">=18.0.0", optional

@types/react-dom is deliberately not added: no published .d.ts in any of the three packages imports from react-dom, so there is nothing to declare.

How to test it?

The manifest change itself is verifiable by inspection, but to see the underlying defect:

  1. In a pnpm workspace, set enableGlobalVirtualStore: true in pnpm-workspace.yaml.
  2. Install @strapi/design-system@2.2.4 and render <SubNavHeader label="…" /> in a .tsx file.
  3. tsc --noEmit → TS2741: Property 'tag' is missing.
  4. tsc --traceResolution | grep "Resolving module 'react' from" → resolution lands on react/index.js, not @types/react.
  5. Apply this manifest via a pnpm packageExtensions override, reinstall, and repeat: @types appears as a sibling in the package's store directory, resolution moves to the consumer's own @types/react, and the errors clear.

Step 5 was confirmed end to end on a different package with the identical defect (react-error-boundary) using exactly this manifest shape.

Repo checks on this branch, on node 22.23.2 / yarn 3.6.4:

  • yarn build — 4/4 tasks pass
  • yarn test:ts — 6/6 tasks pass
  • yarn lint — 6/6 tasks pass (1 pre-existing warning in docs/.storybook/main.ts, untouched by this PR)
  • yarn test:unit — 5/5 tasks pass; 267 tests in @strapi/design-system, 46 in @strapi/ui-primitives, 0 failures

oxfmt does not process .json or .md, so the files changed here are outside its scope; the repo's existing format drift is unrelated to this branch.

A patch changeset is included for all three packages.

Related issue(s)/PR(s)

Same defect and same fix shape, upstream in react-error-boundary — useful as precedent: bvaughn/react-error-boundary#245


Note on base branch: CONTRIBUTING.md asks for PRs against develop, but that branch does not currently exist on this repository (git ls-remote --heads shows only main), and the changesets config uses main as baseBranch — so this targets main. Happy to retarget if that's wrong.

Opened as a draft for maintainer review of the range choice before it lands.

@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

@unrevised6419 is attempting to deploy a commit to the Strapi Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot

changeset-bot Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 16b7c6a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@strapi/design-system Patch
@strapi/ui-primitives Patch
@strapi/icons Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d8dd7e4-88a0-4e74-a16c-172b7a653992

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@unrevised6419
unrevised6419 marked this pull request as ready for review August 12, 2026 14:10

@unrevised6419 unrevised6419 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LGTM

@dosubot dosubot Bot added pr: fix This PR is fixing a bug severity: high If it breaks the basic use of the product source: dependencies relates to dependencies within the repo labels Aug 12, 2026
@unrevised6419

unrevised6419 commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor Author

Confirming this from the consumer side, and flagging one more undeclared module that this PR does not yet cover.

Reproduced on @strapi/design-system 2.2.4 with a scratch consumer installed under pnpm's global virtual store (enableGlobalVirtualStore: true, nodeLinker: isolated, hoist: false), type checked with skipLibCheck: false and strict. That topology is the one that matters here: packages link from a store outside the consuming repo, so TypeScript's upward directory walk from a package's .d.ts never reaches the consumer's node_modules.

In that consumer, react is unreachable from these packages and produces 445 implicit-any errors — 360 from @strapi/icons, 80 from @strapi/design-system, 5 from @strapi/ui-primitives — exactly as described. It also surfaces downstream: @strapi/admin's SubNav.d.ts reports Cannot find name 'C' because the design system's generated types degrade.

The additional gap: @strapi/design-system references @radix-ui/react-context in its published declarations without declaring it in any dependency field.

@strapi/design-system/dist/index.d.ts: error TS2307: Cannot find module '@radix-ui/react-context' or its corresponding type declarations.

It currently resolves only because @strapi/ui-primitives declares it. Unlike the @types/* entries, this one is a real module the package uses, so it wants a plain dependency pinned to whatever already resolves today rather than a peer.

For context, I opened strapi/strapi#27745 for the equivalent problem in the core monorepo packages. The two are independent, but a strict-layout consumer needs both before it type checks cleanly.

…react`

The published `.d.ts` files of `@strapi/design-system`, `@strapi/icons` and
`@strapi/ui-primitives` import React types, but `@types/react` was not declared
as a dependency of any kind. Consumers whose package manager does not place
`@types/react` on the ambient resolution path of these packages resolve `react`
to its untyped runtime entry, so `React.*` degrades to `any` and `Pick`/`Omit`
derived prop types lose optionality.

Declared `@types/react` as an optional peer dependency, mirroring each
package's own `react` peer range.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@HichamELBSI
HichamELBSI force-pushed the fix/missing-types-react-peer-dependency branch from d75888a to 16b7c6a Compare September 24, 2026 15:46
@sonarqubecloud

Copy link
Copy Markdown

@HichamELBSI

Copy link
Copy Markdown
Collaborator

Thanks for the detailed write-up and the repro — the root-cause analysis is solid and the fix (optional @types/react peer, mirroring each package's own react range) is the right pattern here.

LGTM to merge as-is. Could you open a separate issue (or a follow-up PR) for the @radix-ui/react-context gap you flagged in your comment above? It's a distinct problem (a real runtime dependency, not a types-only one) and shouldn't block this fix from landing.

@HichamELBSI
HichamELBSI merged commit 60098cb into strapi:main Sep 24, 2026
8 of 10 checks passed
@unrevised6419
unrevised6419 deleted the fix/missing-types-react-peer-dependency branch September 24, 2026 15:53
@unrevised6419

Copy link
Copy Markdown
Contributor Author

Hey @HichamELBSI, see #2061

HichamELBSI pushed a commit to unrevised6419/design-system that referenced this pull request Sep 25, 2026
…eclared

`dist/components/Select/SelectParts.d.ts` references
`import('@radix-ui/react-context').Scope` in the public types of `SingleSelect`
and `MultiSelect`, but `@strapi/design-system` declared `@radix-ui/react-context`
in no dependency field. It resolved only by accident, through the copy
`@strapi/ui-primitives` hoists under npm/yarn.

On a strict layout — pnpm's global virtual store, `node-linker=isolated`,
`hoist=false` — the upward `node_modules` walk from the published `.d.ts` never
leaves the store, and consumers get:

    SelectParts.d.ts(35,32): error TS2307: Cannot find module
    '@radix-ui/react-context' or its corresponding type declarations.

Unlike `@types/react` in strapi#2046, this is a real module the package's own types
depend on, so it is a plain dependency, pinned to `1.0.1` — the version
`@strapi/ui-primitives` already declares and the one that resolves today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HichamELBSI added a commit that referenced this pull request Sep 25, 2026
…eclared (#2061)

Follow-up to #2046, as requested in
#2046 (comment).

### What does it do?

Declares `@radix-ui/react-context` as a regular dependency of
`@strapi/design-system`, pinned to `1.0.1`:

```json
"dependencies": {
  "@radix-ui/react-checkbox": "1.0.4",
+ "@radix-ui/react-context": "1.0.1",
  "@radix-ui/react-dialog": "1.0.5",
}
```

Manifest-only. No source, build or runtime change.

### Why is it needed?

`@strapi/design-system` names `@radix-ui/react-context` in its published
type declarations but declares it in no dependency field.
`dist/components/Select/SelectParts.d.ts`:

```ts
declare const Root: {
    (props: Select.SelectProps & {
        __scopeSelect?: import('@radix-ui/react-context').Scope;
    }): import("react/jsx-runtime").JSX.Element;
    displayName: string;
};
```

The `Scope` type leaks out of `@strapi/ui-primitives` through the
`__scopeSelect` prop, and `tsc` writes it as an inline `import(...)`
specifier against the *consuming* package's resolution path. Both
occurrences (lines 35 and 43) sit on the public surface — `index.d.ts` →
`./components` → `./Select` → `SingleSelect.d.ts` → `import * as
SelectParts from './SelectParts'` — so any consumer that type checks
`SingleSelect` or `MultiSelect` reaches them.

Today it resolves only by accident. `@strapi/ui-primitives` declares
`@radix-ui/react-context: 1.0.1`, and under npm/yarn hoisting that copy
lands at the top level of the consumer's `node_modules`, where the
upward walk from the design system's `.d.ts` happens to find it. Nothing
in this package's manifest asks for that, and nothing guarantees it.

On a layout without that hoist it breaks. Reproduced on published
`2.2.4` with pnpm 11.26.0, `enableGlobalVirtualStore: true`,
`node-linker=isolated`, `hoist=false`, TypeScript 5.6.3, `strict`,
`skipLibCheck: false`:

```
<store>/@strapi/design-system/2.2.4/<hash>/node_modules/@strapi/design-system/dist/components/Select/SelectParts.d.ts(35,32): error TS2307: Cannot find module '@radix-ui/react-context' or its corresponding type declarations.
<store>/@strapi/design-system/2.2.4/<hash>/node_modules/@strapi/design-system/dist/components/Select/SelectParts.d.ts(43,32): error TS2307: Cannot find module '@radix-ui/react-context' or its corresponding type declarations.
```

`--traceResolution` shows why — packages link from a store outside the
consuming project, so the walk never reaches the consumer's
`node_modules`:

```
======== Resolving module '@radix-ui/react-context' from '<store>/@strapi/design-system/2.2.4/<hash>/node_modules/@strapi/design-system/dist/components/Select/SelectParts.d.ts'. ========
Loading module '@radix-ui/react-context' from 'node_modules' folder, target file types: TypeScript, JavaScript, Declaration, JSON.
Searching all ancestor node_modules directories for preferred extensions: TypeScript, Declaration.
Directory '.../@strapi/design-system/dist/components/Select/node_modules' does not exist, skipping all lookups in it.
Directory '.../@strapi/design-system/dist/components/node_modules' does not exist, skipping all lookups in it.
Directory '.../@strapi/design-system/dist/node_modules' does not exist, skipping all lookups in it.
Directory '.../@strapi/design-system/node_modules' does not exist, skipping all lookups in it.
File '<store>/@strapi/design-system/2.2.4/<hash>/node_modules/@radix-ui/react-context.d.ts' does not exist.
Directory '<store>/@strapi/design-system/2.2.4/node_modules' does not exist, skipping all lookups in it.
...
File '<consumer>/node_modules/@types/radix-ui__react-context.d.ts' does not exist.
```

This is the same class of defect as #2046, but the remedy differs.
`@types/react` is a types-only package the consumer supplies, so it is
an optional peer. `@radix-ui/react-context` is a real module this
package's own public types depend on, so it wants a plain `dependency`.

#### On the version choice

`1.0.1` is what `@strapi/ui-primitives` declares and therefore what
already resolves for every consumer today. Pinning exactly matches the
convention of the other 22 `@radix-ui/*` entries in this manifest, all
of which are exact. This is deliberately a no-op for the installed tree
— it declares the edge that is already there rather than moving anyone
to a new version.

Worth noting for whoever lands #2033 (`chore: migrate from @radix-ui/*
to unified radix-ui package`): if that migration goes through, this
entry folds into the unified `radix-ui` dependency along with the rest.
The two do not conflict — this is the correct manifest for the current
dependency shape, and #2033 rebases over one extra line.

### How to test it?

The manifest change is verifiable by inspection. To see the underlying
defect and the fix:

1. In a scratch consumer, set `enableGlobalVirtualStore: true` in
`pnpm-workspace.yaml` and `node-linker=isolated`, `hoist=false` in
`.npmrc`.
2. Install `@strapi/design-system@2.2.4` and reference `SingleSelect` in
a `.tsx` file.
3. `tsc --noEmit` with `skipLibCheck: false` → the two `TS2307` errors
above.
4. Apply this manifest via a pnpm `packageExtensions` override,
reinstall, repeat → `@radix-ui/react-context` appears in the package's
own store directory, resolution succeeds, both errors clear.

Step 4 was confirmed end to end against published `2.2.4`: 2 `TS2307`
errors before, 0 after, no other diagnostic changed.

Repo checks on this branch, on node 22.23.2 / yarn 3.6.4:

- `yarn build` — 4/4 tasks pass
- `yarn test:ts` — 6/6 tasks pass
- `yarn lint` — 6/6 tasks pass (1 pre-existing warning in
`docs/.storybook/main.ts`, untouched by this PR)
- `yarn test:unit` — 5/5 tasks pass; 32 suites, 267 tests in
`@strapi/design-system`, 0 failures

A `patch` changeset is included for `@strapi/design-system`.

### Related issue(s)/PR(s)

- #2046 — same family of defect (undeclared module named in published
declarations), types-only variant. This was flagged in
#2046 (comment)
and split out at maintainer request.
- #2033 — unified `radix-ui` package migration; see the note above.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr: fix This PR is fixing a bug severity: high If it breaks the basic use of the product source: dependencies relates to dependencies within the repo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants