Skip to content

Fix types for .cjs build #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 6, 2025
Merged

Fix types for .cjs build #16

merged 3 commits into from
Aug 6, 2025

Conversation

joshkel
Copy link
Contributor

@joshkel joshkel commented Jul 28, 2025

Are the Types Wrong is currently reporting errors for memize:

Problems

  • ❌ No types: Import resolved to JavaScript files, but no type declarations were found.
"memize"
node10
node16 (from CJS) ❌ No types
node16 (from ESM) ✅ (ESM)
bundler

For more information, see Are the Types Wrong's UntypedResolution.md. Note that the solution is not to point both the CJS and ESM builds at the same .d.ts file, for reasons explained in Are the Types Wrong's FalseESM.md. Quoting that page:

A golden rule of declaration files is that if they represent a module—that is, if they use import or export at the top level—they must represent exactly one JavaScript file. They especially cannot represent JavaScript files of two different module formats.

The easiest solution I found was to run tsc a second time to build a .d.cts file. For this second invocation, I pointed it at the Rollup-generated .cjs file. (This is because tsc and Rollup by default handle default exports differently for CJS files: tsc translates export default foo to a default attribute on an object with __esModule: true and requires that you instead use export = for a "true" CommonJS export, while Rollup by default creates a "true" default export. By pointing tsc at the Rollup-generated .cjs file, tsc sees Rollup's export =.)

[Are the Types Wrong](https://arethetypeswrong.github.io/?p=memize%402.1.0) is currently reporting errors for memize:

> Problems
>
> * ❌ No types: Import resolved to JavaScript files, but no type declarations were found.
>
>
> ||"memize"
> |---|---|
> |node10	|✅|
> |node16 (from CJS)	|❌ No types|
> |node16 (from ESM)	|✅ (ESM)|
> |bundler	|✅|

For more information, see Are the Types Wrong's [UntypedResolution.md](https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/UntypedResolution.md). Note that the solution is not to point both the CJS and ESM builds at the same `.d.ts` file, for reasons explained in Are the Types Wrong's [FalseESM.md](https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md). Quoting that page:

> A golden rule of declaration files is that if they represent a module—that is, if they use import or export at the top level—they must represent exactly one JavaScript file. They _especially_ cannot represent JavaScript files of two different module formats.

The easiest solution I found was to run `tsc` a second time to build a `.d.cts` file.  For this second invocation, I pointed it at the Rollup-generated `.cjs` file.  (This is because tsc and Rollup by default handle default exports differently for CJS files: tsc translates `export default foo` to a `default` attribute on an object with `__esModule: true` and requires that you instead use `export =` for a "true" CommonJS export, while Rollup [by default](https://rollupjs.org/configuration-options/#output-exports) creates a "true" default export. By pointing tsc at the Rollup-generated `.cjs` file, tsc sees Rollup's `export =`.)
Copy link
Owner

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @joshkel ! Thanks for submitting this. At a glance, this makes sense to me. I was a bit curious about the choice to target the .cjs built output in tsconfig.decl.cts.json, but your explanation makes sense, and I expect it should be stable enough.

In the future, I hope to be able to go ESM-only, as support for require(esm) goes as far back as Node.js v20, but I think this is good as an interim step that doesn't require breaking changes for Node.js support.

I'll plan to give this a more thorough review soon (no later than this weekend), and hopefully publish a fixed version shortly thereafter.

Co-authored-by: Andrew Duthie <[email protected]>
@aduth
Copy link
Owner

aduth commented Aug 3, 2025

Hi @joshkel , I think these changes make sense, but I'm having trouble verifying the actual effect of these changes with a sample project linked to the changes from your branch.

Would you be able to put together a sample reproducible code which demonstrates the problem of types not being loaded loaded under node16, and/or the types being effectively loaded and enforced after your changes?

Here's what I tried:

package.json

{
  "name": "memize-tmp",
  "version": "1.0.0",
  "type": "commonjs",
  "dependencies": {
    "typescript": "^5.9.2"
  },
  "devDependencies": {
    "@types/node": "^24.1.0"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "module": "node16",
    "moduleResolution": "node16",
    "strict": true
  },
  "files": ["index.ts"]
}

index.ts:

const memize = require("memize");
memize("wrong");

Running npm exec tsc under Node v24 (and Node v16) produces no errors before or after the changes.

Since we follow the default extensions, we shouldn't need to specify `types`.
@joshkel
Copy link
Contributor Author

joshkel commented Aug 4, 2025

const memize = require("memize") results in type any (at least when I test it).

If I use your tsconfig.json and the following index.ts:

import memize from 'memize';

const memoizedFunction = memize((x: number) => {
  console.log('Calculating...');
  return x * 2;
});

console.log(memoizedFunction(5)); // Calculating... 10
console.log(memoizedFunction(5)); // 10 (cached result)

Then the import statement gives the following error:

Could not find a declaration file for module 'memize'. '/Users/josh/.nvm/versions/node/v22.17.1/lib/node_modules/memize/dist/index.cjs' implicitly has an 'any' type.
Try npm i --save-dev @types/memize if it exists or add a new declaration (.d.ts) file containing declare module 'memize';ts(7016)

(You can try https://github.com/joshkel/memize-types-test-case, if that helps, and assuming I didn't make any mistakes in putting it together.)

@aduth
Copy link
Owner

aduth commented Aug 6, 2025

Ah, I think the key thing was using import instead of require 🤦 I can reproduce both the original issue and the fix now.

Copy link
Owner

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works great! Thanks again for flagging this issue and coming up with a fix.

@aduth aduth merged commit b77bc39 into aduth:master Aug 6, 2025
@aduth
Copy link
Owner

aduth commented Aug 6, 2025

This is now published as [email protected] 🚀

@joshkel
Copy link
Contributor Author

joshkel commented Aug 7, 2025

Thanks!

@joshkel joshkel deleted the cjs-type-fixes branch August 7, 2025 13:11
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