microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.92k stars 12.47k forks source link

Inconsistent typechecking with require() in JS and TS #60032

Open u130b8 opened 1 month ago

u130b8 commented 1 month ago

πŸ”Ž Search Terms

require import js ts module esm esmodule cjs commonjs

πŸ•— Version & Regression Information

⏯ Playground Link

Multiple files not supported in playground, see bug workbench

πŸ’» Code

// @types: ["node"]
// @allowJs
// @checkJs

// @filename: module-cjs-js.js
const Value = "module-cjs-js";
module.exports = { Value };

// @filename: module-cjs-ts.ts
const Value = "module-cjs-ts";
module.exports = { Value };

// @filename: module-esm-js.js
const Value = "module-esm-js";
export { Value };

// @filename: module-esm-ts.ts
const Value = "module-esm-ts";
export { Value };

// @filename: main-js.js
const ConstRequireCjsJs = require("./module-cjs-js");
const ConstRequireEsmJs = require("./module-esm-js");
const ConstRequireCjsTs = require("./module-cjs-ts");
const ConstRequireEsmTs = require("./module-esm-ts");
console.log(ConstRequireCjsJs.Value); // (alias) const Value: "module-cjs-js"
//                            ^?
console.log(ConstRequireEsmJs.Value); // (alias) const Value: "module-esm-js"
//                            ^?
console.log(ConstRequireCjsTs.Value); // Error: Property 'Value' does not exist on type 'typeof import("./module-cjs-ts")'
//                            ^?
console.log(ConstRequireEsmTs.Value); // (alias) const Value: "module-esm-ts"
//                            ^?

import * as ImportFromCjsJs from "./module-cjs-js";
import * as ImportFromEsmJs from "./module-esm-js";
import * as ImportFromCjsTs from "./module-cjs-ts";
import * as ImportFromEsmTs from "./module-esm-ts";
console.log(ImportFromCjsJs.Value); // (alias) const Value: "module-cjs-js"
//                          ^?
console.log(ImportFromEsmJs.Value); // (alias) const Value: "module-esm-js"
//                          ^?
console.log(ImportFromCjsTs.Value); // Error: Property 'Value' does not exist on type 'typeof import("./module-cjs-ts")'
//                          ^?
console.log(ImportFromEsmTs.Value); // (alias) const Value: "module-esm-ts"
//                          ^?

// @filename: main-ts.ts
const ConstRequireCjsJs = require("./module-cjs-js");
const ConstRequireEsmJs = require("./module-esm-js");
const ConstRequireCjsTs = require("./module-cjs-ts");
const ConstRequireEsmTs = require("./module-esm-ts");
console.log(ConstRequireCjsJs.Value); // any
//                            ^?
console.log(ConstRequireEsmJs.Value); // any
//                            ^?
console.log(ConstRequireCjsTs.Value); // any
//                            ^?
console.log(ConstRequireEsmTs.Value); // any
//                            ^?

import * as ImportFromCjsJs from "./module-cjs-js";
import * as ImportFromEsmJs from "./module-esm-js";
import * as ImportFromCjsTs from "./module-cjs-ts";
import * as ImportFromEsmTs from "./module-esm-ts";
console.log(ImportFromCjsJs.Value); // (alias) const Value: "module-cjs-js"
//                          ^?
console.log(ImportFromEsmJs.Value); // (alias) const Value: "module-esm-js"
//                          ^?
console.log(ImportFromCjsTs.Value); // Error: Property 'Value' does not exist on type 'typeof import("./module-cjs-ts")'
//                          ^?
console.log(ImportFromEsmTs.Value); // (alias) const Value: "module-esm-ts"
//                          ^?

import ImportRequireCjsJs = require("./module-cjs-js");
import ImportRequireEsmJs = require("./module-esm-js");
import ImportRequireCjsTs = require("./module-cjs-ts");
import ImportRequireEsmTs = require("./module-esm-ts");
console.log(ImportRequireCjsJs.Value); // (alias) const Value: "module-cjs-js"
//                             ^?
console.log(ImportRequireEsmJs.Value); // (alias) const Value: "module-esm-js"
//                             ^?
console.log(ImportRequireCjsTs.Value); // Error: Property 'Value' does not exist on type 'typeof import("./module-cjs-ts")'
//                             ^?
console.log(ImportRequireEsmTs.Value); // (alias) const Value: "module-esm-ts"
//                             ^?

Workbench Repro

πŸ™ Actual behavior

Type resolution is inconsistent when using require() from .js and .ts files:

MainExt Type Ext RequireOrImport Issue
JS CJS JS const X = require("Y")
JS ESM JS const X = require("Y")
JS CJS TS const X = require("Y") Error
JS ESM TS const X = require("Y")
JS CJS JS import * as X from "Y"
JS ESM JS import * as X from "Y"
JS CJS TS import * as X from "Y" Error
JS ESM TS import * as X from "Y"
JS CJS JS const X = require("Y")
JS ESM JS const X = require("Y")
JS CJS TS const X = require("Y") Error
JS ESM TS const X = require("Y")
TS CJS JS const X = require("Y") Any
TS ESM JS const X = require("Y") Any
TS CJS TS const X = require("Y") Any
TS ESM TS const X = require("Y") Any
TS CJS JS import * as X from "Y"
TS ESM JS import * as X from "Y"
TS CJS TS import * as X from "Y" Error
TS ESM TS import * as X from "Y"
TS CJS JS import X = require("Y")
TS ESM JS import X = require("Y")
TS CJS TS import X = require("Y") Error
TS ESM TS import X = require("Y")

πŸ™‚ Expected behavior

I expected require() to be typechecked in .ts files because it's typechecked in .js files. I expected imports of CommonJS .ts files to work because CommonJS .js files work and are typechecked.

Additional information about the issue

This problem happens when porting an existing Node.JS codebase that uses CommonJS require() modules to TypeScript. It's not possible to port the code without also forcing it into ES Modules because:

YehuditLiba commented 1 month ago

Have you tried changing your tsconfig.json settings and using esModuleInterop?"

RyanCavanaugh commented 1 month ago

I expected require() to be typechecked in .ts files because it's typechecked in .js files.

Only import i = require( is supported in TS

I expected imports of CommonJS .ts files to work because CommonJS .js files work and are typechecked.

What specifically is the repro for this?

u130b8 commented 1 month ago

Bug workbench doesn't seem to work with types: ["node"], but you can repro it like this:

// @types: ["node"]

// @filename: test-cjs.ts
module.exports = { Value: 1 };

// @filename: main.ts
import test = require("./test-cjs");
test.Value; //  Error: Property 'Value' does not exist on type 'typeof import("./test-cjs")'
//   ^? any

If the module has a JS extension, then the import works. But if the module has a TS extension, it no longer works.

Only import i = require( is supported in TS

Is it possible to add support for const i = require( as well? It seems strange because the functionality is there, the const require works and gets typechecked, but only with a JS extension, why not have it work if the file extension is TS as well? It would make the developer experience much better, and const require is much more flexible than import require:


// Can't do this with import X = require()
let X;
if (cond) {
    X = require("x-impl-1");
}
else {
    X = require("x-impl-2");
}
typescript-bot commented 1 month ago

:wave: Hi, I'm the Repro bot. I can help narrow down and track compiler bugs across releases! This comment reflects the current state of the repro in the issue body running against the nightly TypeScript.


Issue body code block by @u130b8

:x: Failed: -

Historical Information
Version Reproduction Outputs
5.2.2, 5.3.2, 5.4.2, 5.5.2, 5.6.2

:x: Failed: -

  • Cannot find name 'module'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
  • File '/home/runner/work/TypeScript/TypeScript/module-cjs-ts.ts' is not a module.
  • File '/home/runner/work/TypeScript/TypeScript/module-cjs-ts.ts' is not a module.
  • Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
  • Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
  • Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
  • Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
  • File '/home/runner/work/TypeScript/TypeScript/module-cjs-ts.ts' is not a module.
  • Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead.
  • Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead.
  • Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead.
  • File '/home/runner/work/TypeScript/TypeScript/module-cjs-ts.ts' is not a module.
  • Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead.

typescript-bot commented 1 month ago

:wave: Hi, I'm the Repro bot. I can help narrow down and track compiler bugs across releases! This comment reflects the current state of this repro running against the nightly TypeScript.


Comment by @u130b8

:x: Failed: -

Historical Information
Version Reproduction Outputs
5.2.2, 5.3.2, 5.4.2, 5.5.2, 5.6.2

:x: Failed: -

  • Cannot find name 'module'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
  • Import assignment cannot be used when targeting ECMAScript modules. Consider using 'import * as ns from "mod"', 'import {a} from "mod"', 'import d from "mod"', or another module format instead.
  • File '/home/runner/work/TypeScript/TypeScript/test-cjs.ts' is not a module.