ts-node icon indicating copy to clipboard operation
ts-node copied to clipboard

ts-node (ESM mode) incorrectly loads CJS types instead of ESM types for default exports with nested conditions exports

Open yelliver opened this issue 11 months ago • 1 comments

Search Terms

ts-node, ESM, exports, default export, import, require, wrong types

Expected Behavior

When using ts-node in ESM mode, it should correctly resolve type definitions from the import.types field in package.json when importing a default export.

Actual Behavior

When exports in package.json is nested and contains both import and require, ts-node appears to load type definitions from require.types instead of import.types. This causes default exports to fail, while named exports work correctly.

Steps to Reproduce the Problem

  1. Create a TypeScript library sample-library with the following index.ts:

    export function getMessage(): string {
        return "This is a named export";
    }
    
    export default function getDefaultMessage(): string {
        return "This is the default export";
    }
    
  2. Compile the TypeScript files and set up package.json with the following nested exports configuration:

    "exports": {
        ".": {
            "import": {
                "types": "./dist/index.d.ts",  // Expected to be used in ESM mode
                "import": "./dist/index.js"
            },
            "require": {
                "types": "./dist/index.d.cts", // Suspected to be incorrectly loaded
                "require": "./dist/index.cjs"
            }
        }
    }
    
  3. Install sample-library in another project and try to run the following code with ts-node in ESM mode:

    import getDefaultMessage, { getMessage } from "sample-library";
    
    console.log(getMessage()); // Works
    console.log(getDefaultMessage()); // Fails
    
  4. Run the script:

    node --loader ts-node/esm src/index.ts
    
  5. Observe that getDefaultMessage() fails, but getMessage() works correctly.

Minimal Reproduction

❌ DOES NOT WORK

"exports": {
    ".": {
        "import": {
            "types": "./dist/index.d.ts",  // Expected to be used in ESM mode
            "import": "./dist/index.js"
        },
        "require": {
            "types": "./dist/index.d.cts", // Suspected to be incorrectly loaded
            "require": "./dist/index.cjs"
        }
    }
}

✅ WORKS (if require.types matches import.types)

"exports": {
    ".": {
        "import": {
            "types": "./dist/index.d.ts", // Expected to be used in ESM mode
            "import": "./dist/index.js"
        },
        "require": {
            "types": "./dist/index.d.ts", // Now the same as "import.types", making it work
            "require": "./dist/index.cjs"
        }
    }
}

❌ DOES NOT WORK (removing require still fails)

"exports": {
    ".": {
        "import": {
            "types": "./dist/index.d.ts", // Expected to work, but still fails
            "import": "./dist/index.js"
        }
    }
}

✅ WORKS (partially nested structure)

"exports": {
    ".": {
        "types": "./dist/index.d.ts", // Correctly resolved
        "import": "./dist/index.js"
    }
}

✅ WORKS (completely flat structure)

"exports": {
    "types": "./dist/index.d.ts", // Correctly resolved
    "import": "./dist/index.js"
}

Specifications

  • ts-node version: 10.9.2
  • node version: 22.2.0
  • TypeScript version: 5.8.2
  • tsconfig.json, if you're using one:
{}
  • package.json:
{
  "type": "module",
  "dependencies": {
    "ts-node": "^10.9.2",
    "typescript": "^5.8.2",
    "sample-library": "file:../sample-library"
  }
}
  • Operating system and version: macOS 15.3
  • If Windows, are you using WSL or WSL2?: (Fill in if applicable)

Workaround

There are two possible solutions:

  1. Use the same declaration file for both require.types and import.types:

    "exports": {
        ".": {
            "import": {
                "types": "./dist/index.d.ts",
                "import": "./dist/index.js"
            },
            "require": {
                "types": "./dist/index.d.ts", // Use the same .d.ts file here
                "require": "./dist/index.cjs"
            }
        }
    }
    
  2. Avoid nesting import and require within exports. Instead, use a flat or minimally nested structure:

    "exports": {
        ".": {
            "types": "./dist/index.d.ts",
            "import": "./dist/index.js",
            "require": "./dist/index.cjs"
        }
    }
    

yelliver avatar Mar 17 '25 09:03 yelliver

Running into this issue as well with:

Specifications

  • ts-node version: 10.9.2
  • node version: 22.14.0
  • TypeScript version: 5.8.3

Other "workaround" is to add this flag: TS_NODE_TRANSPILE_ONLY=true, but only if this suits your needs though

nicolasroger17 avatar Apr 17 '25 15:04 nicolasroger17