Skip to content

The generated declarations do not resolve under Node's module resolution - #47

Open
leandromatos wants to merge 1 commit into
garmin:mainfrom
leandromatos:fix/declaration-extensions
Open

The generated declarations do not resolve under Node's module resolution#47
leandromatos wants to merge 1 commit into
garmin:mainfrom
leandromatos:fix/declaration-extensions

Conversation

@leandromatos

Copy link
Copy Markdown

Thank you for publishing the SDK with its TypeScript declarations. Having the types ship with the
package is what makes it pleasant to work with, and this report is about one thing keeping them
from reaching part of the audience.

I read the earlier pull requests here and understand this repository publishes the SDK rather than
developing it, so the diff attached is a demonstration of the expected output and not a change to
merge. What follows is the reproduction and the cause, and the fix belongs wherever these files are
generated.

What happens

The package's public symbols do not reach a TypeScript project that resolves modules the way Node
does:

error TS2305: Module '"@garmin/fitsdk"' has no exported member 'Decoder'.
error TS2305: Module '"@garmin/fitsdk"' has no exported member 'Stream'.

The symbols are there at runtime. Only the types are lost, so the workaround is a hand-written
declare module '@garmin/fitsdk' in the consuming project, which is the situation shipped
declarations exist to remove.

Who it affects

Consumers compiling with "moduleResolution": "node16" or "nodenext", the settings that match
Node's own ESM resolution. Consumers on "moduleResolution": "bundler" are unaffected, which is
probably why this has not surfaced: this repository's tsconfig.json uses bundler, so the
declarations type-check here exactly as they do not for a consumer on Node resolution.

Reproduction

Verified against @garmin/fitsdk@21.214.0 with TypeScript 6.0.3 on Node 24.

mkdir fitsdk-repro && cd fitsdk-repro
npm init -y
npm install @garmin/fitsdk@21.214.0 typescript@6.0.3

tsconfig.json:

{
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "noEmit": true
  }
}

index.ts:

import { Decoder, Stream } from '@garmin/fitsdk'

export const isFitFile = (bytes: Uint8Array): boolean => Decoder.isFIT(Stream.fromByteArray(bytes))

Compiling reports 12 errors: two TS2305 in index.ts, one per imported symbol, and ten TS2834
in src/index.d.ts, one per re-export.

src/index.d.ts(13,15): error TS2834: Relative import paths need explicit file extensions in
ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'.

Adding "skipLibCheck": true brings it down to 2. The TS2834 disappear and the two TS2305 at
the import site remain, now with nothing left to explain them, which is the state most consumers
land in.

Cause

src/index.d.ts re-exports the generated declarations with extensionless relative specifiers:

export * from './types/decoder';

package.json sets "type": "module" and declares no exports field, so those declarations are
ESM. Under Node's ESM resolution a relative specifier carries its extension, so './types/decoder'
does not resolve. TypeScript reports that inside the declaration file, skipLibCheck hides the
report, and the consumer is left with an entry point that exports nothing.

The declarations under src/types have the same shape, so nested types degrade even when a
consumer reaches a declaration file through a deep path. decoder.d.ts imports "./stream",
"./mesg" and "./mesgs", for example.

What would fix it

Emitting .js on every relative specifier in the generated .d.ts files, matching what
src/index.js already does for the runtime modules:

-export * from './types/decoder';
+export * from './types/decoder.js';

The attached diff does exactly that. It touches 20 specifiers across six files, src/index.d.ts
and the five under src/types that carry relative imports, and takes the reproduction from 12
errors to 0. pnpm test in this repository still reports 616 passing tests and no type errors.

One detail worth flagging for whoever changes the generator: the specifiers appear in two forms.
src/index.d.ts uses single quotes and export * from, while the files under src/types use
double quotes and import { X } from. Handling only the first form leaves 10 of the 12 errors in
place, which reads like the fix did not work.

The same project on "moduleResolution": "bundler" reports 0 both before and after, so the
extension costs current consumers nothing.

These files carry a "Do NOT edit this file" banner, so this belongs in the generator rather than in
the checked-in output. Happy to test a pre-release if that is useful.

@leandromatos
leandromatos requested a review from a team as a code owner September 1, 2026 15:52
@Lijah99

Lijah99 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

I was able to recreate the module resolution errors you described.

Thank you for brining this to our attention. We will release a fix for this in the next public release sometime next week. As you mentioned, the SDK source code is generated from internal Garmin repositories, so changing code via a PR would eventually be overwritten.

I'll reach out on this PR and close it once the new version of the SDK is published.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants