Skip to content

CJS type mismatch when using @langfuse/langchain with TypeScript nodenext module resolution #748

@RingoDev

Description

@RingoDev

Issue

In our NestJS project (TypeScript 5.9.3, "module": "nodenext"), using CallbackHandler from @langfuse/langchain in a callbacks array causes type errors. TypeScript reports that CallbackHandler is not assignable to BaseCallbackHandler, even though CallbackHandler extends BaseCallbackHandler at runtime. The error message shows two different resolution paths for the same @langchain/core types: one with { with: { "resolution-mode": "import" } } and one without.

This seems to happen because our project resolves @langchain/core types via CJS (.d.cts), but the CallbackHandler type from @langfuse/langchain references BaseCallbackHandler resolved via ESM (.d.ts). TypeScript treats these as two distinct types.

Proposed solution

I use a postinstall script that patches the exports map in node_modules/@langfuse/langchain/package.json.
This nests the types condition under import/require in each package's exports map instead of having it as a sibling.

Current:

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

Proposed:

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

The exports maps seem hand-written so I suspect the change would need to be applied in each package's package.json directly, e.g. packages/langchain/package.json, packages/otel/package.json, packages/tracing/package.json, and any other packages using the same sibling pattern.

Why this works

When types is a sibling of import/require, TypeScript resolves it first regardless of the consumer's module format. Since langfuse packages have "type": "module", the .d.ts file is treated as ESM-authored, and any dependency types it re-exports (like @langchain/core's BaseCallbackHandler) are resolved via ESM. A CJS consumer resolves those same dependencies via CJS, producing two nominally distinct versions of the same type.

The .d.cts files already ship with the packages but are never referenced in the exports map. Nesting types under each condition lets TypeScript pick .d.cts for CJS consumers and .d.ts for ESM consumers, so dependency types align on both sides. This is the pattern recommended in the TypeScript 4.7 announcement for dual packages that ship separate type declarations per format.

Related

This was reported in #716 and closed when upgrading to TypeScript 5.9.3 appeared to resolve it for the reporter. The structural issue remains. I am on TS 5.9.3 and still hit it.

Metadata

Metadata

Assignees

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