Export and import directives have several syntax variants.
In the previous article we saw a simple use, now let’s explore more examples.
Export before declarations
We can label any declaration as exported by placingexport before it, be it a variable, function or a class.
For instance, here all exports are valid:
// export an arrayexport let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];// export a constantexport const MODULES_BECAME_STANDARD_YEAR = 2015;// export a classexport class User { constructor(name) { this.name = name; }}Please note thatexport before a class or a function does not make it afunction expression. It’s still a function declaration, albeit exported.
Most JavaScript style guides don’t recommend semicolons after function and class declarations.
That’s why there’s no need for a semicolon at the end ofexport class andexport function:
export function sayHi(user) { alert(`Hello, ${user}!`);} // no ; at the endExport apart from declarations
Also, we can putexport separately.
Here we first declare, and then export:
// 📁 say.jsfunction sayHi(user) { alert(`Hello, ${user}!`);}function sayBye(user) { alert(`Bye, ${user}!`);}export {sayHi, sayBye}; // a list of exported variables…Or, technically we could putexport above functions as well.
Import *
Usually, we put a list of what to import in curly bracesimport {...}, like this:
// 📁 main.jsimport {sayHi, sayBye} from './say.js';sayHi('John'); // Hello, John!sayBye('John'); // Bye, John!But if there’s a lot to import, we can import everything as an object usingimport * as <obj>, for instance:
// 📁 main.jsimport * as say from './say.js';say.sayHi('John');say.sayBye('John');At first sight, “import everything” seems such a cool thing, short to write, why should we ever explicitly list what we need to import?
Well, there are few reasons.
- Explicitly listing what to import gives shorter names:
sayHi()instead ofsay.sayHi(). - Explicit list of imports gives better overview of the code structure: what is used and where. It makes code support and refactoring easier.
Modern build tools, such aswebpack and others, bundle modules together and optimize them to speedup loading. They also remove unused imports.
For instance, if youimport * as library from a huge code library, and then use only few methods, then unused oneswill not be included into the optimized bundle.
Import “as”
We can also useas to import under different names.
For instance, let’s importsayHi into the local variablehi for brevity, and importsayBye asbye:
// 📁 main.jsimport {sayHi as hi, sayBye as bye} from './say.js';hi('John'); // Hello, John!bye('John'); // Bye, John!Export “as”
The similar syntax exists forexport.
Let’s export functions ashi andbye:
// 📁 say.js...export {sayHi as hi, sayBye as bye};Nowhi andbye are official names for outsiders, to be used in imports:
// 📁 main.jsimport * as say from './say.js';say.hi('John'); // Hello, John!say.bye('John'); // Bye, John!Export default
In practice, there are mainly two kinds of modules.
- Modules that contain a library, pack of functions, like
say.jsabove. - Modules that declare a single entity, e.g. a module
user.jsexports onlyclass User.
Mostly, the second approach is preferred, so that every “thing” resides in its own module.
Naturally, that requires a lot of files, as everything wants its own module, but that’s not a problem at all. Actually, code navigation becomes easier if files are well-named and structured into folders.
Modules provide a specialexport default (“the default export”) syntax to make the “one thing per module” way look better.
Putexport default before the entity to export:
// 📁 user.jsexport default class User { // just add "default" constructor(name) { this.name = name; }}There may be only oneexport default per file.
…And then import it without curly braces:
// 📁 main.jsimport User from './user.js'; // not {User}, just Usernew User('John');Imports without curly braces look nicer. A common mistake when starting to use modules is to forget curly braces at all. So, remember,import needs curly braces for named exports and doesn’t need them for the default one.
| Named export | Default export |
|---|---|
export class User {...} | export default class User {...} |
import {User} from ... | import User from ... |
Technically, we may have both default and named exports in a single module, but in practice people usually don’t mix them. A module has either named exports or the default one.
As there may be at most one default export per file, the exported entity may have no name.
For instance, these are all perfectly valid default exports:
export default class { // no class name constructor() { ... }}export default function(user) { // no function name alert(`Hello, ${user}!`);}// export a single value, without making a variableexport default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];Not giving a name is fine, because there is only oneexport default per file, soimport without curly braces knows what to import.
Withoutdefault, such an export would give an error:
export class { // Error! (non-default export needs a name) constructor() {}}The “default” name
In some situations thedefault keyword is used to reference the default export.
For example, to export a function separately from its definition:
function sayHi(user) { alert(`Hello, ${user}!`);}// same as if we added "export default" before the functionexport {sayHi as default};Or, another situation, let’s say a moduleuser.js exports one main “default” thing, and a few named ones (rarely the case, but it happens):
// 📁 user.jsexport default class User { constructor(name) { this.name = name; }}export function sayHi(user) { alert(`Hello, ${user}!`);}Here’s how to import the default export along with a named one:
// 📁 main.jsimport {default as User, sayHi} from './user.js';new User('John');And, finally, if importing everything* as an object, then thedefault property is exactly the default export:
// 📁 main.jsimport * as user from './user.js';let User = user.default; // the default exportnew User('John');A word against default exports
Named exports are explicit. They exactly name what they import, so we have that information from them; that’s a good thing.
Named exports force us to use exactly the right name to import:
import {User} from './user.js';// import {MyUser} won't work, the name must be {User}…While for a default export, we always choose the name when importing:
import User from './user.js'; // worksimport MyUser from './user.js'; // works too// could be import Anything... and it'll still workSo team members may use different names to import the same thing, and that’s not good.
Usually, to avoid that and keep the code consistent, there’s a rule that imported variables should correspond to file names, e.g:
import User from './user.js';import LoginForm from './loginForm.js';import func from '/path/to/func.js';...Still, some teams consider it a serious drawback of default exports. So they prefer to always use named exports. Even if only a single thing is exported, it’s still exported under a name, withoutdefault.
That also makes re-export (see below) a little bit easier.
Re-export
“Re-export” syntaxexport ... from ... allows to import things and immediately export them (possibly under another name), like this:
export {sayHi} from './say.js'; // re-export sayHiexport {default as User} from './user.js'; // re-export defaultWhy would that be needed? Let’s see a practical use case.
Imagine, we’re writing a “package”: a folder with a lot of modules, with some of the functionality exported outside (tools like NPM allow us to publish and distribute such packages, but we don’t have to use them), and many modules are just “helpers”, for internal use in other package modules.
The file structure could be like this:
auth/ index.js user.js helpers.js tests/ login.js providers/ github.js facebook.js ...We’d like to expose the package functionality via a single entry point.
In other words, a person who would like to use our package, should import only from the “main file”auth/index.js.
Like this:
import {login, logout} from 'auth/index.js'The “main file”,auth/index.js exports all the functionality that we’d like to provide in our package.
The idea is that outsiders, other programmers who use our package, should not meddle with its internal structure, search for files inside our package folder. We export only what’s necessary inauth/index.js and keep the rest hidden from prying eyes.
As the actual exported functionality is scattered among the package, we can import it intoauth/index.js and export from it:
// 📁 auth/index.js// import login/logout and immediately export themimport {login, logout} from './helpers.js';export {login, logout};// import default as User and export itimport User from './user.js';export {User};...Now users of our package canimport {login} from "auth/index.js".
The syntaxexport ... from ... is just a shorter notation for such import-export:
// 📁 auth/index.js// re-export login/logoutexport {login, logout} from './helpers.js';// re-export the default export as Userexport {default as User} from './user.js';...The notable difference ofexport ... from compared toimport/export is that re-exported modules aren’t available in the current file. So inside the above example ofauth/index.js we can’t use re-exportedlogin/logout functions.
Re-exporting the default export
The default export needs separate handling when re-exporting.
Let’s say we haveuser.js with theexport default class User and would like to re-export it:
// 📁 user.jsexport default class User { // ...}We can come across two problems with it:
export User from './user.js'won’t work. That would lead to a syntax error.To re-export the default export, we have to write
export {default as User}, as in the example above.export * from './user.js're-exports only named exports, but ignores the default one.If we’d like to re-export both named and default exports, then two statements are needed:
export * from './user.js'; // to re-export named exportsexport {default} from './user.js'; // to re-export the default export
Such oddities of re-exporting a default export are one of the reasons why some developers don’t like default exports and prefer named ones.
Summary
Here are all types ofexport that we covered in this and previous articles.
You can check yourself by reading them and recalling what they mean:
- Before declaration of a class/function/…:
export [default] class/function/variable ...
- Standalone export:
export {x [as y], ...}.
- Re-export:
export {x [as y], ...} from "module"export * from "module"(doesn’t re-export default).export {default [as y]} from "module"(re-export default).
Import:
- Importing named exports:
import {x [as y], ...} from "module"
- Importing the default export:
import x from "module"import {default as x} from "module"
- Import all:
import * as obj from "module"
- Import the module (its code runs), but do not assign any of its exports to variables:
import "module"
We can putimport/export statements at the top or at the bottom of a script, that doesn’t matter.
So, technically this code is fine:
sayHi();// ...import {sayHi} from './say.js'; // import at the end of the fileIn practice imports are usually at the start of the file, but that’s only for more convenience.
Please note that import/export statements don’t work if inside{...}.
A conditional import, like this, won’t work:
if (something) { import {sayHi} from "./say.js"; // Error: import must be at top level}…But what if we really need to import something conditionally? Or at the right time? Like, load a module upon request, when it’s really needed?
We’ll see dynamic imports in the next article.