You can see my sample project here: https://github.com/DanKaplanSES/typescript-stub-examples/tree/JavaScript-import-invalid
I have created this file called main.ts:
import uuid from "uuid";
console.log(uuid.v4());
Although typescript is fine with this import, when I try to node main.js
, it gives this error:
console.log(uuid_1["default"].v4());
^
TypeError: Cannot read property 'v4' of undefined
at Object.<anonymous> (C:\root\lib\main.js:5:31)
←[90m at Module._compile (internal/modules/cjs/loader.js:1063:30)←[39m
←[90m at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)←[39m
←[90m at Module.load (internal/modules/cjs/loader.js:928:32)←[39m
←[90m at Function.Module._load (internal/modules/cjs/loader.js:769:14)←[39m
←[90m at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)←[39m
←[90m at internal/main/run_main_module.js:17:47←[39m
If I change the file to this, it executes fine:
import * as uuid from "uuid";
console.log(uuid.v4());
If the first version is invalid, why doesn't typescript inform me?
I have a multi file tsconfig setup. Check the github project for more details, but here are the shared compiler options which may be relevant:
{
"compilerOptions": {
"rootDir": ".",
"esModuleInterop": true,
"module": "CommonJS",
"moduleResolution": "node",
"composite": true,
"importHelpers": true,
},
}
Here is how the main.js looks:
"use strict";
exports.__esModule = true;
var tslib_1 = require("tslib");
var uuid_1 = tslib_1.__importDefault(require("uuid"));
console.log(uuid_1["default"].v4());
"use strict";
exports.__esModule = true;
var tslib_1 = require("tslib");
var uuid = tslib_1.__importStar(require("uuid"));
console.log(uuid.v4());
Importing Types With TypeScript 3.8, you can import a type using the import statement, or using import type .
One of the major differences between require() and import() is that require() can be called from anywhere inside the program whereas import() cannot be called conditionally, it always runs at the beginning of the file. To use the require() statement, a module must be saved with .
I kind of feel guilty answering my own bounty question, so I'm going to mark it as community. The reason I'm writing my own is because I feel like the other answer really buries the lead. I had to perform hours and hours of my own research after reading it to be able to write this. That being the case, I think my answer will be more helpful to people that start in my boat, where I didn't know what I didn't know. I also think there's an additional solution to the problem, though it would be more invasive.
My original question was, "If the first version is invalid, why doesn't typescript inform me?" Here is the other answer's explanation:
Because you have enabled esModuleInterop which also enables allowSyntheticDefaultImports. The CommonJS bundle is actually incompatible with that option but TypeScript doesn't know.
This is absolutely true, but when it comes to understanding what's going on, it's the tip of the iceberg:
If you look at the reference documentation, it recommends you set esModuleInterop
to true. Why would it make that recommendation if it reduces type safety? Well, that is not why it recommends you set it to true. In fact, this setting does not reduce type safety -- it increases it by fixing some legacy typescript bugs, specifically two that deal with how typescript handles requires
. You can read the documentation for more details on that, but in my opinion, if you are using node libraries, I think it's a good idea to set esModuleInterop to true.
But! esModuleInterop has a side effect. At the very bottom of its documentation, it says:
Enabling esModuleInterop will also enable allowSyntheticDefaultImports.
Err... kinda. IMO, this documentation is incorrect. What it should really say is, "Enabling esModuleInterop will default allowSyntheticDefaultImports to true." If you look at the allowSyntheticDefaultImports documentation, it says this on the right-hand side:
Hey, notice how in that upper right-hand corner it doesn't say this setting is recommended? That's probably because this setting reduces type safety: it allows you to type import React from "react";
instead of import * as React from "react";
when the module does not explicitly specify a default export.
Normally (i.e. with allowSyntheticDefaultImports set to false), this would be an error... because it is: you shouldn't be able to default import a module unless it has a default export. Setting this to true makes the compiler say, "Nah, this is fine."
But, when you set allowSyntheticDefaultImports to true, "this flag does not affect the JavaScript emitted by TypeScript." What this means is, this flag lets you pretend the library was written one way at compile time even though it wasn't. At runtime, this will error. Why does the setting even exist? I have no idea, but it probably has to do with historical reasons:
This option brings the behavior of TypeScript in-line with Babel, where extra code is emitted to make using a default export of a module more ergonomic.
It seems like there was(/is?) a point in time where everybody was just assumed to be using Babel. I am not doing that, so the "ergonomic" benefit becomes a runtime error.
As a cleaner method, you should import uuid with import { v4 } from 'uuid';
True, but I think it would also be a good idea to explicitly set allowSyntheticDefaultImports to false. It gives you more type safety. Not only that, it makes import uuid from "uuid";
a compile time error (which it should be).
There is one more thing I don't understand:
Setting allowSyntheticDefaultImports to false also makes imports like import os from "os";
and import _ from "lodash";
compile time errors. But those always ran fine when allowSyntheticDefaultImports was true. There must be some piece I'm missing that explains why those work but uuid
doesn't.
I can't find the source of os
in my node_modules, but I can look at lodash
, and its index.js
does this:
module.exports = require('./lodash');
In that required file, it says this at the bottom:
...
/*--------------------------------------------------------------------------*/
// Export lodash.
var _ = runInContext();
// Some AMD build optimizers, like r.js, check for condition patterns like:
if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
// Expose Lodash on the global object to prevent errors when Lodash is
// loaded by a script tag in the presence of an AMD loader.
// See http://requirejs.org/docs/errors.html#mismatch for more details.
// Use `_.noConflict` to remove Lodash from the global object.
root._ = _;
// Define as an anonymous module so, through path mapping, it can be
// referenced as the "underscore" module.
define(function() {
return _;
});
}
// Check for `exports` after `define` in case a build optimizer adds it.
else if (freeModule) {
// Export for Node.js.
(freeModule.exports = _)._ = _;
// Export for CommonJS support.
freeExports._ = _;
}
else {
// Export to the global object.
root._ = _;
}
I don't really understand what all of this is doing, but I think this is defining a global variable named _
at runtime? I guess that means, from a typescript perspective, this coincidentally works out. The type declaration files don't have a default, which would normally cause a runtime error, but almost coincidentally, it all works out in the end because the lodash javascript defines a global _
? shrug Maybe this is a pattern that os
uses, too, but I've already spent enough hours researching this, so I will leave that for another day/question.
Your issue is related to interoperability between TypeScript/ECMAScript modules and CommonJS.
When it comes to the differences between ECMAScript modules and CommonJS modules:
const library = require('library')
which allows to retrieve the full exports
object of that library. There is no notion of default import in CommonJS
export
clauses for every exported item. They also feature a default import syntax which allows to retrieve the default
export in a local variable.In order to implement interoperability between CommonJS modules and TypeScript's default import syntax, CommonJS modules can have a default
property.
That default
property can even be added automatically by TypeScript when esModuleInterop
is enabled (which also enables allowSyntheticDefaultImports
). This option adds this helper function at transpilation time:
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Basically what this function does is: if the imported module has the __esModule
flag set to true, export it as is because the module is intended to be used as an ECMAScript module: import { feature } from 'library'
. Otherwise, export it inside a wrapper object with a default
property, which enables the import localName from 'library'
syntax.
The uuid
package is being built with @babel/plugin-transform-modules-commonjs
which includes the __esModule
flag and prevents you from using the default import syntax. Other packages like lodash
don't include this flag, which allows TypeScript to add the default
property.
As a conclusion, TypeScript provides options to interoperate with legacy CommonJS modules but these options don't work with "ECMAScript aware" CommonJS modules. TypeScript cannot warn or error out at transpilation time because a CommonJS module interface has no representation other than the exports
object, which is only known at runtime.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With