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

Cannot export both types and values from CJS #60852

Open
kirkwaiblinger opened this issue Dec 25, 2024 · 1 comment
Open

Cannot export both types and values from CJS #60852

kirkwaiblinger opened this issue Dec 25, 2024 · 1 comment
Assignees
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@kirkwaiblinger
Copy link

🔎 Search Terms

verbatimModuleSyntax, commonjs, require, export, export type,

🕗 Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/play/?moduleResolution=99&target=99&module=199&verbatimModuleSyntax=true&ts=5.8.0-dev.20241225#code/KYDwDg9gTgLgBDAnmYcBiEJwLxwM4xQCWAdgOZwA+cJArgLYBGwUA3AFDsDGANgIZ48cAEJ8ocAN4BfTqEiwcIsayA

💻 Code

I wanted to convert a *.ts file that gets interpreted as CJS (due to package.json "type" field) to use verbatimModuleSyntax for explicitness.

The file previously looked like this

// index.ts
export type Foo = string | number;
export const Bar = 42;

and could be imported like so

// foo.ts
import { type Foo, Bar } from './index';

const x: Foo = 42;
const y = Bar;

After enabling verbatimModuleSyntax, it seems no longer possible to export both Foo and Bar from index.ts. Some attempts:

type Foo = string | number;
const Bar = 42;

export = { Foo, Bar }; // error; Foo is a type but used as a value

This is ok,

export type Foo = string | number;
const Bar = 42;

but this is not

export type Foo = string | number;
const Bar = 42;
export = { Bar }; // An export assignment cannot be used in a module with other exported elements. (TS2309)

nor this

type Foo = string | number;
const Bar = 42;
export type { Foo };
export = { Bar }; // An export assignment cannot be used in a module with other exported elements. (TS2309)

tsconfig:

{
    "compilerOptions": {
        "verbatimModuleSyntax": true,
        "target": "ESNext",
        "module": "NodeNext",
        "strict": true,
    }
}

package.json: has "type": "commonjs"

🙁 Actual behavior

Can't find a way to export both types and values using CJS import/export syntax required by verbatimModuleSyntax

🙂 Expected behavior

There is a way to export both types and values using CJS import/export syntax

Additional information about the issue

Perhaps this is possible, and if so I'd ask it be added to the documentation. Without this, a currently-functioning CJS module cannot straightforwardly be converted to use verbatimModuleSyntax.

FWIW, if it's not possible, I don't see why at least one of the following wouldn't be allowed (specifically due to the use of export type to ensure only one value export exists)

export type Foo = string | number;
const Bar = 42;
export = { Bar }; // not a problem; only one value export

or

type Foo = string | number;
const Bar = 42;
export type { Foo };
export = { Bar };
@kirkwaiblinger
Copy link
Author

kirkwaiblinger commented Dec 31, 2024

Followup, I have come across the following excerpt from #52203:

We knew this would be a bit of a burden for imports, but I didn’t realize what a pain it would be for exports. The problem here is that you can no longer sidecar-export a type:

export type T = {};
export = { hello: "world" };
// ^^^^^^^^^^^^^^^^^^^^^^^^
// An export assignment cannot be used in a module with other exported elements.(2309)

To accomplish this, you’d have to merge a namespace declaration containing the types:

const _exports = { hello: "world" };
namespace _exports {
  export type T = {};
}
export = _exports;

These limitations make it really unattractive to use the flag in CJS-emitting TypeScript. I discussed this with @DanielRosenwasser, and ultimately decided that it’s not worth worrying about too much—the flag is opt-in, and we expect the users interested in using it to write mostly ESM-emitting TypeScript.

Given this information it's apparently a known issue that CJS export syntax is painful. I'd consider this issue a request to make it less painful.

While the quoted excerpt above implies that CJS is not a first-class citizen of verbatimModuleSyntax, I did not get that takeaway from the release notes, which I actually understood to be encouraging verbatimModuleSyntax-with-CJS to avoid confusion, particularly this part:

While this is a limitation, it does help make some issues more obvious. For example, it’s very common to forget to set the type field in package.json under --module node16. As a result, developers would start writing CommonJS modules instead of ES modules without realizing it, giving surprising lookup rules and JavaScript output. This new flag ensures that you’re intentional about the file type you’re using because the syntax is intentionally different.

Similarly on the issue proposal:

This import is legal under --module esnext, but an error in --module commonjs. (In node16 and nodenext, it depends on the file extension and/or the package.json "type" field.) If the file is determined to be a CommonJS module at emit by any of these settings, it must be written as

import fs = require("fs");
instead. Many users have the impression that this syntax is legacy or deprecated, but that’s not the case. It accurately reflects that the output will use a require statement, instead of obscuring the output behind layers of transformations and interop helpers. I think using this syntax is particularly valuable in .cts files under --module nodenext, because in Node’s module system, imports and requires have markedly different semantics, and actually writing out require helps you understand when and why you can’t require an ES module—it’s easier to lose track of this when your require is disguised as an ESM import in the source file.


My personal initial impression working with verbatimModuleSyntax is that it is super-super worth it for CJS. The minutiae of node's CJS/ESM interop are extremely nuanced and it is extremely easy to lose track of what's happening at all once you throw TS's module syntax transformations into the mix. I've wasted many hours this way, before even realizing that half my assumptions were wrong because I was unknowingly writing CJS (like the release notes mention!). Once I realized that, I wasted many more hours 🙃.

Anyways, so I would strongly request some improvements around DevX. Ideas for making type exports better:

  • allow "sidecar" type-only exports
  • allow export type { Foo as Bar }; syntax in CJS, where you can supply named type exports.
    const foo = 'bar';
    type FooType = string;
    export = { foo };
    export type { FooType };

Both of these would be equivalent to the "merged namespace" approach.

With CJS it's very important IMO to be extremely clear what the exported value is. So I'm happy for types and values not to be allowed to mix in the exports. But I don't see any reason why type-only export syntax which is currently banned should not be allowed to exist in parallel to the value export syntax. Users don't expect type-only syntax to have runtime impact, so I don't see the possibility for confusion.

Similarly, on the importing side of it, I'd propose

  • import type { Foo, Bar } from './foo.cts' be allowed (including from cts file) (but NOT import { type Foo } from './foo.cts', even if all the imports are type-only. Only import type.
  • allow destructuring syntax in import = require() syntax. require type modifiers for types under verbatimModuleSyntax, and prohibit a statement which does not have at least one destructured (non-type) value (regardless of verbatimModuleSyntax).
    import { value, type FooType } = require('./foo.cts');
    to allow colocating type and value imports (and generally improve the runtime const { namedValue } = require('./foo.js'); situation). This is a superset of Allow destructuring in import assignment #46752

@RyanCavanaugh RyanCavanaugh added Needs Investigation This issue needs a team member to investigate its status. Suggestion An idea for TypeScript In Discussion Not yet reached consensus and removed Needs Investigation This issue needs a team member to investigate its status. labels Jan 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants