Skip to content

Types: exports map points both conditions at the CJS declaration — default import unusable under moduleResolution: NodeNext (4.6.0+) #816

Description

@binhpv

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:

// package.json (4.7.1)
"exports": {
  ".": {
    "types": "./build/cjs/cjs.d.ts",
    "import": "./build/cjs/cjs.mjs",
    "require": "./build/cjs/cjs.js"
  },
  "./package.json": "./package.json"
}

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions