Skip to content

declaration emit can trigger TS2742 when public types are local aliases re-exported later #958

@unional

Description

@unional

Summary

tsdown emits declarations for tersify that cause downstream consumers to fail with TS2742 during declaration emit, while tsc declarations from the same source tree do not.

This repro is preserved in a private staging repo with a submodule pointing at a dedicated tersify branch:

  • cyberuni/tsdown-ts2742-repro
  • submodule branch: cyberuni/tsdown-types-export-regression

Repro

From tersify/packages/tersify:

pnpm smoke:consumer:tsdown
pnpm smoke:consumer:tsc

Current result:

  • pnpm smoke:consumer:tsdown fails
  • pnpm smoke:consumer:tsc passes

Downstream consumer

import { tersible } from 'tersify'

export function foo() {
  return tersible((x: any) => !!x)
}

Consumer tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "declaration": true,
    "emitDeclarationOnly": true,
    "strict": true,
    "skipLibCheck": false
  }
}

Error

index.ts(3,17): error TS2742: The inferred type of 'foo' cannot be named without a reference to './node_modules/tersify/esm/types.mjs'. This is likely not portable. A type annotation is necessary.

Important constraint

The tersify source files are the same in both cases.
Only the emitted declarations differ.

Narrowed cause

The issue is not caused by:

  • the source condition in package.json
  • .mjs alone
  • root index re-export shape alone
  • tersible.d.mts alone

The decisive difference is the emitted shape of esm/types.d.mts.

This emitted pattern fails downstream:

type TersifyOptions = ...
type Tersible<T = unknown> = ...
export { Tersible, TersifyOptions }

This also fails:

type TersifyOptions = ...
type Tersible<T = unknown> = ...
export type { Tersible, TersifyOptions }

This passes:

export type TersifyOptions = ...
export type Tersible<T = unknown> = ...

Working hypothesis

When tsdown emits a declaration file with public types as local aliases plus a trailing export list, TypeScript 5.0 does not treat those symbols as a portable public origin during downstream inference of exported return types.

In this case that leads to TS2742, while direct exported type declarations do not.

Environment

Verified in this setup:

  • Host OS: Ubuntu 24.04.3 LTS
  • Kernel: Linux 6.6.114.1-microsoft-standard-WSL2
  • Architecture: x64
  • Node: v22.21.1
  • pnpm: 10.33.4
  • npm: 10.9.4

Package under test:

  • tersify: 4.0.0
  • tsdown: 0.22.0
  • package-local typescript: 6.0.2

Downstream consumer used in the smoke repro:

  • typescript: 5.0.2

Notes

The downstream repro intentionally installs typescript@5.0.2 in the consumer project, because that is the compiler producing the reported TS2742.

So there are two TypeScript versions involved:

  • tersify package devDependency: 6.0.2
  • downstream consumer compiler: 5.0.2

The current repro was verified on WSL2 / Ubuntu, but the issue appears to be driven by emitted declaration shape rather than the host OS itself.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions