When you start working with JavaScript or TypeScript, you’ll notice two different styles of importing libraries:
// Style 1
import * as moment from "moment";
// Style 2
import moment from "moment";
At first glance, these two look similar. But depending on the project, one may work while the other gives you an error.
So why do some packages “require” import * as
? And when can you use the cleaner import …
style?
The answer lies in module systems and TypeScript compiler settings.
Module Systems: CommonJS vs ES Modules
There are two main ways JavaScript code is packaged:
1. CommonJS (CJS)
The original Node.js system. Uses require
and module.exports
.
// moment/index.js
module.exports = moment;
// Using require
const moment = require("moment");
2. ES Modules (ESM)
The modern JavaScript standard. Uses import
and export
.
// modern-style module
export default PDFDocument;
// Using import
import PDFDocument from "pdfkit";
👉 Most older libraries like Moment.js, PDFKit, and Lodash were written in CommonJS, not ESM.
What import * as …
Does
When you write:
import * as moment from "moment";
This means:
“Import the entire module object and assign it to the variable
moment
.”
If the module only exports one thing (like Moment.js does), you’ll still get the whole object under that name.
✅ This always works with CommonJS libraries.
❌ It feels a little verbose.
What import …
Does
When you write:
import moment from "moment";
This means:
“Import the default export from the module.”
But here’s the catch: CommonJS libraries don’t technically have a default export.
That’s why in TypeScript you need to enable some flags in your tsconfig.json
:
{
"compilerOptions": {
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
}
}
These options tell TypeScript:
👉 “Pretend that CommonJS modules export a default object so we can use import x from "y";
.”
Examples in Action
Let’s see how this plays out with a few popular libraries.
Example 1: Moment.js
// Works everywhere
import * as moment from "moment";
console.log(moment().format("YYYY-MM-DD"));
// Cleaner, but requires esModuleInterop
import moment from "moment";
console.log(moment().format("YYYY-MM-DD"));
Example 2: PDFKit
// Namespace import
import * as PDFDocument from "pdfkit";
const doc = new PDFDocument();
doc.text("Hello, PDFKit!");
doc.end();
// Default import (needs esModuleInterop)
import PDFDocument from "pdfkit";
const doc = new PDFDocument();
doc.text("Hello, PDFKit with default import!");
doc.end();
Example 3: Lodash
// Namespace import
import * as _ from "lodash";
console.log(_.capitalize("hello world"));
// Default import (needs esModuleInterop)
import _ from "lodash";
console.log(_.capitalize("hello world"));
Why Some Projects Use * as …
- They have strict TypeScript settings (
esModuleInterop: false
). - They want to stay compatible with pure CommonJS modules.
- Older codebases often stick with this style.
Why Some Projects Use Default Imports
- They enabled
esModuleInterop: true
. - They prefer the cleaner syntax:
import moment from "moment";
import PDFDocument from "pdfkit";
import _ from "lodash";
- It matches modern ESM conventions.
Which One Should You Use?
- ✅ For new projects: enable
esModuleInterop
intsconfig.json
and useimport x from "package"
. - ✅ For legacy or strict projects: use
import * as x from "package"
. - ✅ For modern libraries (like Luxon, Axios, etc.): always use
import x from "package"
— they’re written as ES Modules.
The difference isn’t about Moment.js, PDFKit, or Lodash themselves.
It’s about how TypeScript bridges the gap between CommonJS and ES Modules.
-
import * as X
→ Import the entire module object (safe for all CommonJS packages). -
import X
→ Import the default export (only works if you enableesModuleInterop
).
👉 So, if a package seems to “require” * as
, it’s not the package’s fault — it’s just your project’s module configuration.
Top comments (0)