Skip to content

fix: declare the @types packages the published types import - #5821

Open
unrevised6419 wants to merge 1 commit into
styled-components:mainfrom
unrevised6419:fix/declare-types-peer-dependencies
Open

unrevised6419 wants to merge 1 commit into
styled-components:mainfrom
unrevised6419:fix/declare-types-peer-dependencies

Conversation

@unrevised6419

@unrevised6419 unrevised6419 commented Sep 20, 2026 •

Copy link
Copy Markdown

Problem

The shipped .d.ts files reference types the package never declares:

  • react → needs @types/react
  • react-dom/server (PipeableStream in ServerStyleSheet) → needs @types/react-dom
  • the NodeJS.ReadableStream / NodeJS.ReadWriteStream globals in the same public signature → needs @types/node

Under a hoisted node_modules these resolve by accident, from the consuming app's own tree. Under an isolated layout they do not — pnpm's default linker with a global virtual store links each package from a store path outside the consuming repo, and TypeScript resolves a package's imports from where that package physically lives. react then lands on the untyped react/index.js, every styled component's props collapse to any, and noImplicitAny consumers get errors that skipLibCheck: true had been hiding inside the .d.ts.

Reported from a consumer app that hit it on 6.5.3 and worked around it with a packageExtensions entry.

Fix

@types/react, @types/react-dom and @types/node are added as optional peer dependencies. The react / react-dom ranges are unchanged. Optional matters: JavaScript-only consumers must not get an unmet-peer warning, and a fresh install of a JS-only consumer confirms none is emitted.

The two React types floors mirror the runtime ones at >= 19.0.0, which is also what the public types need: they name React.JSX.Element and React.JSX.IntrinsicElements, and @types/react-dom constrains @types/react itself. @types/node stays * on purpose — the only thing the types reach for is the NodeJS stream globals, present in every major, so a floor there would assert a constraint the package does not have and would age into a false warning.

No @types/stylis — v7 emits CSS through the in-house parser and has no stylis dependency; dependencies is csstype alone.

Verification

Packed the tarball and installed it into two scratch consumers under pnpm with the virtual store outside the project (enableGlobalVirtualStore), typechecking with skipLibCheck: false and noImplicitAny:

const Box = styled.div<{ $gap: number }>`gap: ${p => p.$gap}px;`;
// @ts-expect-error — collapses to `any` if @types/react is unreachable, and then this stops erroring
<Box $gap="not-a-number" />;

Without the declarations:

index.tsx(6,1): error TS2578: Unused '@ts-expect-error' directive.
dist/constructors/createGlobalStyle.d.ts(1,19): error TS7016: Could not find a declaration file for
  module 'react'. '.../react/index.js' implicitly has an 'any' type.
dist/models/ServerStyleSheet.d.ts(2,37): error TS7016: Could not find a declaration file for
  module 'react-dom/server'.

With them, tsc exits 0 and --traceResolution shows react, react-dom/server and csstype each resolving to a .d.ts.

The @ts-expect-error form is deliberate — a positive assertion keeps passing once the types widen to any, so it would not catch a regression.

Full suites pass on the change: typecheck, test:types, 1473 web tests, 2527 native tests.

No automated test ships with this. A manifest-only assertion would restate package.json without proving the types resolve, and the check that does prove it — pack, install into an isolated-store consumer, typecheck with skipLibCheck: false — needs a real install per run. Happy to add it to the build suite if you want that cost.

Out of scope, noted while sweeping

publint flags ESM files served as CJS and the absent exports map; attw flags CJS default-export interop on the native subpaths. Both predate this change and are breaking to fix, so they are left alone.

@changeset-bot

changeset-bot Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5f34da4

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

This PR includes changesets to release 1 package
Name Type
styled-components 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

@unrevised6419
unrevised6419 force-pushed the fix/declare-types-peer-dependencies branch 2 times, most recently from 4af2244 to f1af2f6 Compare September 20, 2026 00:29

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ No blocking issues. One ordering nit and a couple of test-quality notes inline.

Reviewed changes

  • Optional @types/* peers — @types/node, @types/react, @types/react-dom are declared as optional peer dependencies so TypeScript resolves the packages the published .d.ts files import from where styled-components physically lives, not only under a hoisted tree.
  • packaging-types.test.ts — new suite locking that every dependency/peer shipping no types has an @types sibling declared, that @types/node is present while sources name NodeJS., and that every @types peer is marked optional.
  • Changeset — patch entry describing the consumer-visible fix.

I verified the peer additions do not require a lockfile change (pnpm install --frozen-lockfile reports "Lockfile is up to date", since the @types/* are already devDependencies and pnpm dedups them), confirmed each dependency/peer's type-shipping status, and checked there is no stylis type import and no other NodeJS. use outside ServerStyleSheet.tsx.

ℹ️ The new test guards the manifest, not the resolution behavior

The regression only surfaces under an isolated linker with skipLibCheck: false; the jest suite runs hoisted, so packaging-types.test.ts can assert the package.json contract but not that tsc actually resolves react, react-dom/server and NodeJS from the package location. That is an acceptable scope given the manual packed-tarball verification in the PR description — flagging it only so the guard is not mistaken for behavioral coverage of the consumer failure mode.

ℹ️ Nitpicks

  • The test docblock says the assertions lock "anything the public types reach for is named in package.json". The first it only reaches packages already listed in dependencies/peerDependencies, so it would not catch a brand-new external import that is undeclared entirely. Narrowing that wording would keep the comment honest.
  • shipsOwnTypes calls require.resolve(\${name}/package.json`)` for every dependency and peer. If an optional peer is ever absent from a given install layout, the test throws instead of reporting the offending package. A try/catch that fails with the package name would make the failure legible.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread packages/styled-components/package.json Outdated
The shipped .d.ts files import React's types, PipeableStream from
react-dom/server and the NodeJS stream globals without declaring any of
them. A hoisted node_modules resolves those from the consuming app by
accident; an isolated layout does not, and every styled component's props
degrade to any.
@unrevised6419
unrevised6419 force-pushed the fix/declare-types-peer-dependencies branch from f1af2f6 to 5f34da4 Compare September 20, 2026 00:33

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ No critical issues — one observation below.

Reviewed changes

  • Raised the @types/react and @types/react-dom peer floors from * to >= 19.0.0, matching the react / react-dom runtime peers.
  • Removed packages/styled-components/src/test/packaging-types.test.ts, the manifest-contract suite that guarded the @types/* declarations.
  • Moved the three @types/* keys to the head of peerDependencies and peerDependenciesMeta, so the two objects agree and sort first (addresses the prior review's ordering nit).

I verified the floor change does not re-break resolution: installing the package into a scratch pnpm 10 consumer under an isolated virtual store, a @types/react@18 consumer with the >= 19 peer still has its copy linked into the package's virtual-store node_modules, so TypeScript resolves it. An absent optional peer emits no warning, so the JS-only consumer claim holds.

ℹ️ The manifest contract is no longer guarded

Deleting packaging-types.test.ts leaves nothing in the suite that fails if the @types/* peers are dropped, reordered, or stripped of peerDependenciesMeta.optional. treeshake.test.ts already asserts package.json shape (sideEffects, the browser field, subpath entries) and is the natural home. The caveat from the prior review still stands — jest runs hoisted, so a manifest test cannot prove isolated-linker resolution — but it would pin the declaration this fix depends on, which the repo's strict-TDD mandate otherwise expects.

Technical details
# Restore a manifest guard for the `@types/*` peers

## Affected sites
- `packages/styled-components/src/test/packaging-types.test.ts` — deleted; was the only test asserting the `@types/*` peers and their optional flags.
- `packages/styled-components/src/test/treeshake.test.ts:642` — existing precedent for `package.json`-shape assertions.

## Required outcome
- A suite-level test that fails if any of `@types/react`, `@types/react-dom`, `@types/node` is removed from `peerDependencies`, or loses its `peerDependenciesMeta.optional: true`, or if any untyped dependency lacks a declared `@types` sibling.

## Open questions for the human
- Is the manifest guard wanted at all, or is the packed-tarball manual verification in the PR body considered sufficient for a metadata-only change?

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

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.

1 participant