Skip to content

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

@yelliver

Description

@yelliver

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"
        }
    }

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