Skip to content

feat: add optional @types/react peer dependency - #276

Open
unrevised6419 wants to merge 1 commit into
ndresx:masterfrom
unrevised6419:feat/optional-types-react-peer
Open

unrevised6419 wants to merge 1 commit into
ndresx:masterfrom
unrevised6419:feat/optional-types-react-peer

Conversation

@unrevised6419

@unrevised6419 unrevised6419 commented Sep 24, 2026 •

Copy link
Copy Markdown

Adds @types/react as an optional peer dependency at ">= 18".

The symptom

On a TypeScript app using pnpm with virtualStoreType: global, react-countdown does not degrade quietly — it fails to compile:

TS2786: 'ReactCountdown' cannot be used as a JSX component.
  Type 'Countdown' is missing the following properties from type 'Component<any, any, any>'

Every <Countdown /> usage site errors. The app has @types/react installed and react on a compatible version; nothing about the setup is exotic apart from the store layout.

Why it happens

react-countdown ships .d.ts files that import react for its types, but declares only the react runtime peer. dist/Countdown.d.ts in 2.3.6 starts:

import * as React from 'react';
...
export default class Countdown extends React.Component<CountdownProps, CountdownState> { ... }

Under pnpm's global virtual store the package is not installed inside the consuming repository. It lives in the shared store, e.g.

~/Library/pnpm/store/v11/links/@/react-countdown/2.3.6/<hash>/node_modules/react-countdown

TypeScript resolves a package's imports from where that package physically sits, and it ignores NODE_PATH. Walking up from that store path, the only react it can see is the one pnpm linked in to satisfy the declared runtime peer — the react package itself, which ships no declarations. The consumer's @types/react is never on that path, because nothing told pnpm it was needed there.

tsc --noEmit --traceResolution:

======== Resolving module 'react' from '<store>/react-countdown/dist/Countdown.d.ts'. ========
======== Module name 'react' was successfully resolved to '<store>/react/index.js' with Package ID 'react/index.js@19.2.7'. ========

index.js, not index.d.ts. That is the whole bug. Where the JS fallback is accepted (allowJs: true) it is silent and every React-derived type in the public API becomes any. Where it is not — react's exports map offers no types condition and there is no index.d.ts beside index.js — react resolves to nothing, React.Component is unresolved, the class's base type collapses to {}, and TS2786 fires. That second case is what the error above is.

master is affected in the same way. The v3 declarations still reach into react; pnpm build on 3.0.0-beta.0 emits:

import * as React from 'react';
declare const _default: React.ForwardRefExoticComponent<CountdownProps & React.RefAttributes<CountdownHandle>>;

If react resolves to nothing there, the default export's type collapses and the same class of JSX error follows.

The fix

"peerDependencies": {
  "@types/react": ">= 18",
  "react": ">= 18",
  "react-dom": ">= 18"
},
"peerDependenciesMeta": {
  "@types/react": { "optional": true }
}

That is enough for pnpm to place the consumer's @types/react next to react-countdown wherever it installs it, and the traceResolution line above becomes @types/react/index.d.ts.

Why it is safe

  • Optional, so JavaScript-only consumers are unaffected: no install warning, nothing added to their lockfile.
  • ">= 18", mirroring the react: ">= 18" peer this repo already declares, so the types peer adds no version claim of its own. This repo develops against @types/react@^19, which the range includes.
  • Runtime behaviour does not change at all. Nothing is imported, nothing is bundled, dist is byte-identical.
  • In this repo, pnpm install after the change leaves pnpm-lock.yaml unmodified. pnpm lint and pnpm test (58 tests, 6 snapshots) pass unchanged.

Precedent

@testing-library/react — already a devDependency here — declares exactly this shape, and has since v13:

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

I mirrored the repo's existing react: ">= 18" peer rather than pinning a version: the point is to make the types reachable, not to have react-countdown narrow which @types/react a consumer runs.

I verified this end to end on a real application. Adding these peers for the affected packages via pnpm packageExtensions took it from 61 untyped and 6 unresolved React resolutions to 1012/1012 landing on a .d.ts, with tsc still exiting 0.

Related: #90 reported a very similar-looking JSX error (Type 'Countdown' is missing the following properties from type 'ElementClass') back in 2022. That one had a different root cause and was fixed in v2.2.2, but the failure mode is the same — an unresolvable React in the shipped declarations reads to the consumer as "this component is not a component".

Happy to also add @types/react-dom alongside it if you would prefer to mirror @testing-library/react exactly, though nothing in the current declarations imports react-dom.

@coveralls

coveralls commented Sep 24, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 100.0%. remained the same — unrevised6419:feat/optional-types-react-peer into ndresx:master

@unrevised6419
unrevised6419 force-pushed the feat/optional-types-react-peer branch from 519da88 to 58e9634 Compare September 24, 2026 03:06
The published declarations import `react` for its types, but only the
`react` runtime peer is declared. Package managers that install the
package outside the consumer's repository (pnpm's global virtual store)
therefore leave no `@types/react` reachable from the shipped `.d.ts`
files, and `React.*` resolves to the untyped `react/index.js` or to
nothing at all.

Declaring `@types/react` as an optional peer at `>= 18`, mirroring the
`react` peer this package already declares, makes the types resolvable
without affecting non-TypeScript consumers and without pinning a specific
version.
@unrevised6419
unrevised6419 force-pushed the feat/optional-types-react-peer branch from 58e9634 to 24bdec6 Compare September 24, 2026 03:19
@unrevised6419
unrevised6419 marked this pull request as ready for review September 24, 2026 03:28
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.

2 participants