Skip to content

v19 type declarations incompatible with moduleResolution: "NodeNext" #369

@michealparks

Description

@michealparks

v19 added an exports field to package.json. This activates strict ESM module resolution in TypeScript's NodeNext mode, but the package's .d.ts files still use extensionless relative imports — which NodeNext requires to be explicit (e.g. "./exports.js", "./geometry/index.js").

I'm a member of Threlte and recently ran into this issue when upgrading our Rapier package.

We cannot do a patch workaround since this'll affect our end users.

Root cause

rapier.d.ts is the entry point resolved via the exports field:

import * as RAPIER from "./exports";  // ← no extension                                                                      
export * from "./exports";            // ← no extension                                                                      
export default RAPIER;

With "type": "module" + an exports field, TypeScript treats the package as strict ESM. In NodeNext strict ESM, "./exports" cannot be resolved — TypeScript requires "./exports.js". The export * from "./exports" silently exports nothing, breaking all named imports.

This same pattern repeats throughout every .d.ts file in the package (exports.d.ts, geometry/index.d.ts, dynamics/index.d.ts, etc.).

This worked in v18 because v18 had no exports field, so TypeScript used legacy entry-point resolution which is lenient about extensionless imports inside declaration files.

Reproduction

Any TypeScript project with:

{
  "compilerOptions": {                                                                                                       
    "module": "NodeNext",
    "moduleResolution": "NodeNext"
  }                               
}  
   
import { World, Collider, RigidBody } from '@dimforge/rapier3d-compat'
//       ^^^^^  ^^^^^^^^  ^^^^^^^^^                                                                                          
// Error: Module has no exported member. Did you mean to use 'import X from ...' instead?                                    

Possible fix

Add .js extensions to all relative imports in .d.ts files, and use /index.js for directory imports. For example:

-import * as RAPIER from "./exports";                                                                                        
-export * from "./exports";                                                                                                  
+import * as RAPIER from "./exports.js";
+export * from "./exports.js";                                                                                               
                  
-export * from "./geometry";
+export * from "./geometry/index.js";  

This was the fix that we arrived at: threlte/threlte#1471

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions