Skip to content
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

Use CommonJS export for "module": "node16" + ESM #171

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

karlhorky
Copy link

@karlhorky karlhorky commented Jan 29, 2023

Hi there 👋 First of all, thanks for slugify, it's very useful!

The ESM-style export (export default slugify) causes an error when using the "module": "node16" option in tsconfig.json and "type": "module" in package.json:

$ yarn tsc
index.ts:3:1 - error TS2349: This expression is not callable.
  Type 'typeof import("/home/projects/node-wshdsj/node_modules/slugify/slugify")' has no call signatures.

3 slugify('');
  ~~~~~~~


Found 1 error in index.ts:3

error Command failed with exit code 2.

Demo on StackBlitz - run yarn tsc in the Terminal at the bottom to reproduce:

https://stackblitz.com/edit/node-wshdsj?file=tsconfig.json,package.json,index.ts&file=tsconfig.json,index.ts

Screenshot 2023-01-29 at 18 04 37

Background

The change to ESM-style export in #19 seemed like the right solution at the time, because bundlers such as webpack + ts-loader did not understand the CommonJS syntax (export = slugify).

However, module systems have evolved since then and now the export default fails when "module": "node16" is configured in tsconfig.json inside of a project which is an ES Module ("type": "module" in package.json).

Since slugify is a CommonJS module, it seems like using a CommonJS-style export is indeed the right choice for this package.

See more information about the problem of misconfigured declaration files in packages here:

The change in simov#19 to switch to an ESM-style export seemed like the right solution at the time.

However, module systems have evolved since then and the `export default` fails when "module": "node16" is configured in tsconfig.json

Since `slugify` is a CommonJS module, it seems like using a CommonJS-style export is indeed the right choice for this package.
@karlhorky
Copy link
Author

cc @andrewbranch in case you can see an error in my logic about "module": "node16" + PR above, would be amazing to have a tip here!

@karlhorky karlhorky changed the title Use CommonJS export for "module": "node16" Use CommonJS export for "module": "node16" + ESM Jan 29, 2023
@andrewbranch
Copy link

slugify’s JS uses a UMD wrapper that includes

module.exports = factory()
module.exports['default'] = factory()

so technically the export default in the types isn’t wrong, it’s just incomplete. In your repro, you can do slugify.default('') and TypeScript will recognize that as good, and it will work in Node too. But obviously you want to be able to get at the module exports without the unnecessary .default. To do that, off the top of my head, I would write the types like this:

declare module slugify {
  type ExtendArgs = {
    [key: string]: any;
  }

  export function extend (args: ExtendArgs): void;
  const _default: typeof slugify;
  export { _default as default };
}

declare function slugify(
  string: string,
  options?:
    | {
        replacement?: string;
        remove?: RegExp;
        lower?: boolean;
        strict?: boolean;
        locale?: string;
        trim?: boolean;
      }
    | string,

): string;

export = slugify;
export as namespace slugify;

Note the const _default: typeof slugify; export { _default as default }; in the existing merged namespace as well as the export = slugify change. Together, those cover both the module.exports as well as the module.exports.default. The export as namespace slugify just declares that the module is also available as a global in script files, since it does have a browser-compatible UMD wrapper. That can be dropped if global usage is discouraged.

@andrewbranch
Copy link

Also, I will note that this kind of problem is not yet detectable by https://arethetypeswrong.github.io, but it’s next on my list.

@karlhorky
Copy link
Author

karlhorky commented Feb 1, 2023

Ok great, added the changes for the UMD wrapper in b443c79 . I checked and this seems to also work in my project with "module": "node16"

@simov @Trott what do you think?

@GabrielDelepine
Copy link

GabrielDelepine commented Mar 13, 2023

Edit: I realized that the issue I described in my comment was not caused by the changes proposed in this PR. Actually, merging this PR will be solving my problem

@remcohaszing
Copy link

Is there a reason to use module over namespace here? Also is there any particular reason to define exports in the module instead of an interface?

I think it’s purely stylistic, but I’m curious to know if there are any differences. I would write it like this:

declare namespace slugify {
  type ExtendArgs = {
    [key: string]: any;
  }

  interface Options {
    replacement?: string;
    remove?: RegExp;
    lower?: boolean;
    strict?: boolean;
    locale?: string;
    trim?: boolean;
  }
}

interface Slugify {
  (string: string, options?: slugify.Options | string): string;
  default: Slugify;
  extend: (args: slugify.ExtendArgs) => void;
}

declare const slugify: Slugify;

export = slugify;
export as namespace slugify;

@andrewbranch
Copy link

There’s no difference, and namespace is the preferred keyword. I used module only because I copied and pasted the original code.

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.

4 participants