Summary
Since the tsup build landed in 4.6.0, the exports map declares a single top-level "types" entry that sits outside the import/require conditions:
Both conditions therefore resolve to build/cjs/cjs.d.ts. The package has no "type": "module", so that file is a CommonJS declaration — and under moduleResolution: "node16" | "nodenext" a default import of a CJS module is typed as the whole module.exports object rather than the declared default export. The result is that Draggable cannot be used as a JSX component.
build/cjs/cjs.d.mts — the correct ESM declaration — is shipped in the tarball, but nothing in the exports map can reach it.
This is types-only; the runtime is fine (import D from "react-draggable" gives function Draggable under Node ESM).
Versions
- react-draggable 4.7.1 (introduced in 4.6.0, which added the
exports map)
- typescript 6.0.3 (also reproduces on 5.x)
- @types/react 19.2.18
Reproduction
package.json
{ "name": "repro", "type": "module", "version": "1.0.0" }
tsconfig.json
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"jsx": "react-jsx",
"esModuleInterop": true,
"strict": true,
"noEmit": true
},
"include": ["index.tsx"]
}
index.tsx
import Draggable from "react-draggable";
export const App = () => (
<Draggable>
<div>drag me</div>
</Draggable>
);
npx tsc -p tsconfig.json:
index.tsx(4,3): error TS2604: JSX element type 'Draggable' does not have any construct or call signatures.
index.tsx(4,3): error TS2786: 'Draggable' cannot be used as a JSX component.
Its type 'typeof import(".../react-draggable/build/cjs/cjs")' is not a valid JSX element type.
--traceResolution confirms the ESM import is typed by the CJS declaration:
======== Module name 'react-draggable' was successfully resolved to
'.../react-draggable/build/cjs/cjs.d.ts' with Package ID
'react-draggable/build/cjs/cjs.d.ts@4.7.1'. ========
Suggested fix
Move types inside each condition so the ESM entry gets the .d.mts that is already being built:
"exports": {
".": {
- "types": "./build/cjs/cjs.d.ts",
- "import": "./build/cjs/cjs.mjs",
- "require": "./build/cjs/cjs.js"
+ "import": {
+ "types": "./build/cjs/cjs.d.mts",
+ "default": "./build/cjs/cjs.mjs"
+ },
+ "require": {
+ "types": "./build/cjs/cjs.d.ts",
+ "default": "./build/cjs/cjs.js"
+ }
},
"./package.json": "./package.json"
}
The root main/module/types/typings fields can stay as they are for legacy resolvers. I verified this change locally as a package patch — it clears both errors, with no runtime change.
attw agrees. Published 4.7.1:
🎭 Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. (FalseCJS)
"react-draggable"
node10 🟢
node16 (from CJS) 🟢 (CJS)
node16 (from ESM) 🎭 Masquerading as CJS
bundler 🟢
With the exports change above:
"react-draggable"
node10 🟢
node16 (from CJS) 🟢 (CJS)
node16 (from ESM) 🟢 (ESM)
bundler 🟢
Bundler resolution is unaffected, which is presumably why this has not come up more often — it only bites node16/nodenext consumers.
Possibly related: DraggableEventHandler parameter type
In 4.5.0 the handler took the library's own event union:
export type DraggableEventHandler = (e: DraggableEvent, data: DraggableData) => void | false;
In 4.7.1 it takes the global DOM MouseEvent:
type DraggableEventHandler = (e: MouseEvent, data: DraggableData) => void | false;
type DraggableEvent = React.MouseEvent<HTMLElement | SVGElement> | React.TouchEvent<HTMLElement | SVGElement> | MouseEvent | TouchEvent;
onTouchStart routes touch events into handleDragStart, which calls props.onStart, so a handler can be invoked with a TouchEvent — and with a React synthetic event when the drag starts from the React-attached onMouseDown. The declared parameter type excludes both, while DraggableEvent right above it describes them exactly. If this was an unintended consequence of the Flow → TypeScript migration, DraggableEventHandler taking DraggableEvent would match both the old surface and the runtime behaviour.
Happy to open a PR for either or both if the direction looks right.
Summary
Since the tsup build landed in 4.6.0, the
exportsmap declares a single top-level"types"entry that sits outside theimport/requireconditions:Both conditions therefore resolve to
build/cjs/cjs.d.ts. The package has no"type": "module", so that file is a CommonJS declaration — and undermoduleResolution: "node16" | "nodenext"a default import of a CJS module is typed as the wholemodule.exportsobject rather than the declared default export. The result is thatDraggablecannot be used as a JSX component.build/cjs/cjs.d.mts— the correct ESM declaration — is shipped in the tarball, but nothing in theexportsmap can reach it.This is types-only; the runtime is fine (
import D from "react-draggable"givesfunction Draggableunder Node ESM).Versions
exportsmap)Reproduction
package.json{ "name": "repro", "type": "module", "version": "1.0.0" }tsconfig.json{ "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", "target": "ES2022", "jsx": "react-jsx", "esModuleInterop": true, "strict": true, "noEmit": true }, "include": ["index.tsx"] }index.tsxnpx tsc -p tsconfig.json:--traceResolutionconfirms the ESM import is typed by the CJS declaration:Suggested fix
Move
typesinside each condition so the ESM entry gets the.d.mtsthat is already being built:"exports": { ".": { - "types": "./build/cjs/cjs.d.ts", - "import": "./build/cjs/cjs.mjs", - "require": "./build/cjs/cjs.js" + "import": { + "types": "./build/cjs/cjs.d.mts", + "default": "./build/cjs/cjs.mjs" + }, + "require": { + "types": "./build/cjs/cjs.d.ts", + "default": "./build/cjs/cjs.js" + } }, "./package.json": "./package.json" }The root
main/module/types/typingsfields can stay as they are for legacy resolvers. I verified this change locally as a package patch — it clears both errors, with no runtime change.attwagrees. Published 4.7.1:With the
exportschange above:Bundler resolution is unaffected, which is presumably why this has not come up more often — it only bites
node16/nodenextconsumers.Possibly related:
DraggableEventHandlerparameter typeIn 4.5.0 the handler took the library's own event union:
In 4.7.1 it takes the global DOM
MouseEvent:onTouchStartroutes touch events intohandleDragStart, which callsprops.onStart, so a handler can be invoked with aTouchEvent— and with a React synthetic event when the drag starts from the React-attachedonMouseDown. The declared parameter type excludes both, whileDraggableEventright above it describes them exactly. If this was an unintended consequence of the Flow → TypeScript migration,DraggableEventHandlertakingDraggableEventwould match both the old surface and the runtime behaviour.Happy to open a PR for either or both if the direction looks right.