Learn Advanced JavaScript in about a week!

Learning should be fun and quick; no need to spend ages in just one single course.

Start learning  

#18 Programming
23 mins   /

What exactly are import attributes in JavaScript?

Learn what are import attributes in JavaScript and how to use them for importing JSON modules in browsers and in Node.js.

There was a time when JavaScript had no concept of modules at all and then came in ECMAScript 6. ES6 brought forth a rich set of features to JavaScript, including a robust module system, thusly referred to as ECMAScript modules.

ECMAScript modules marked a big win in the history of JavaScript. But since their advent, the development of features around modules hasn't ended — the TC39 committee is continuously working on improving modules to every extent they could.

In this article, I want to discuss a relatively new feature in ES modules known as import attributes. Let's go!

What are import attributes?

To begin with, let's familiarize ourselves with what exactly are import attributes from a technical standpoint.

Import attributes are a means of providing extra information while importing a module. In theory, this extra info could be anything ranging from "how to import the module" to "how to parse it" to an endless amount of things. The sky's the limit.

However, at the time of this writing, import attributes are only used in JavaScript to natively handle JSON modules, CSS modules, and Wasm (Web Assembly) modules.

The standard anticipates the possible introduction of many other attributes in the future, as their applications arise.

In other words, at the time of this writing, import attributes are mainly used to specify "how to import a module" in JavaScript. Speaking of which, the attribute used in this respect is called type.

We'll learn more about type shortly below. For now, let's see the general form of laying out import attributes in JavaScript.

Syntax of import attributes

What I really like about the syntax of import attributes in JavaScript is that it doesn't replace or improvise the existing import syntax. Rather, it just extends the existing syntax.

That is, after the standard import syntax, we have the with keyword, followed by a pair of curly braces ({}). This pair of braces holds the individual attributes alongside their corresponding values:

import name from specifier with { attr1: value1, ... };

To better understand this trivial syntax, let's consider some real examples using the only attribute available as of now — type.

The type import attribute

As the name suggests, type specifies the type of the underlying module. That is, it states whether the module is a JSON module, a CSS module, or a Wasm module.

The list of possible values of type are:

  • 'json'
  • 'css'
  • 'wasm'
Any other value besides these isn't entertained since it's NOT part of the official standard.

For JavaScript programmers working in Node, 'json' is of particular importance because JSON files are often used for settings and other configurations.

Importing JSON modules natively in browsers

The browser environment has never ever had a native way for importing JSON files. In fact, as you already know, browsers once had no native module system for JavaScript, let alone a way to import JSON files.

The only way developers leveraged JSON imports was by means of build tools and bundlers. For example, back then, a developer would write something like this and expect the necessary processing to be automatically done by the bundling tool (mostly Webpack in those days):

JavaScript
// Code parsed by a bundler like Webpack, Vite, etc.

import jsonData from './foo.json';

These tools concatenated all build files together into one single JavaScript file delivered to the browser. So the JSON data was always embedded directly into the JavaScript, which explains how it all worked.

It was quite simple and straightforward to work with JSON data in this way on the client end. However, the point is that there was no native way of importing JSON modules.

Even following the advent of ECMAScript modules in JavaScript, there was no concept of being able to import JSON files using the import syntax.

If one had to use "idiomatic" JavaScript in the browser for importing a JSON file, here's what was done:

JavaScript
fetch('./foo.json')
   .then(response => response.json())
   .then(jsonData => {
      // Work with jsonData
   });

Of course, this wasn't easy to work with, for the entire set of code that depended upon the JSON file had to reside inside a promise's then() or, slightly more elegantly, following an await call:

JavaScript
let response = await fetch('./foo.json');
let jsonData = await response.json();

// Work with jsonData

Either way, not cool at all!

But thanks to a TC39 proposal to add native support to import a JSON file using the standard import syntax, JavaScript eventually got import attributes.

And now you could easily do the following:

JavaScript
import jsonData from './foo.json' with { type: 'json' };

The with { type: 'json' } syntax instructs the browser to explicitly treat the imported file as JSON data, and thereby load it as a JSON object into jsonData.

Of course, this code assumes that you aren't using a bundler because bundlers typically don't enforce the inclusion of the with {} syntax.

A concrete example for the browser

Suppose we have the following HTML document:

HTML
<!DOCTYPE html>
<html>
<head>
   <title>Working with import attributes</title>
   <script type="module" src="main.js"></script>
</head>
<body>
   <h1>Working with import attributes</h1>
</body>
</html>

And in the same directory where this HTML document dwells, we also have the following JSON file, named languages.json:

languages.json
JSON
[
   {
      "name": "JavaScript",
      "ext": ".js"
   },
   {
      "name": "Python",
      "ext": ".py"
   },
   {
      "name": "C++",
      "ext": ".cpp"
   }
]

It simply contains a list of languages, each with its name and file extension. The job is to import this JSON file directly in the main.js file linked in the HTML document (which resides in the same location as the HTML document and the JSON file).

Alright then, let's do so. It's really simple!

Here's the code (in main.js) to import the JSON file:

main.js
JavaScript
import languages from './languages.json' with { type: 'json' };

Let's inspect languages and see what it contains:

main.js
JavaScript
import languages from './languages.json' with { type: 'json' };

console.log(languages instanceof Array);
console.log(languages);
A screenshot of the console in Google Chrome for the code above
Chrome's console screenshot for the code above

As can be seen, the first log is true which indicates that languages is clearly an array. The second log showcases the contents of languages which is exactly what we have above in the JSON file.

You can even try this example as follows: Live Example

Importing JSON modules natively in Node

If you are an experienced Node developer, you'll know that Node has had the ability to import JSON modules directly since a long time, thanks to the CommonJS module system.

In particular, the call to require() (in the CommonJS module format) is able to import a JSON file, i.e. one whose name ends with the .json extension, directly as JSON data:

JavaScript
const jsonData = require('./languages.json');

However, with the advent of the standard ECMAScript module system, and the hype to shift to it, importing JSON files became somewhat of an issue in Node because require() did NOT work in ECMAScript modules.

Developers using ECMAScript modules in Node had to manually read and parse JSON files using a combination of a file-reading utility like fs.readFileSync() and JSON.parse():

JavaScript
import fs from 'node:fs';

let jsonData = JSON.parse(fs.readFileSync('./foo.json'));

Needless to say, Node developers demanded a native way of importing JSON in ECMAScript modules using the modern import syntax, without having to resort to such workarounds.

Fast-forward to today, Node supports native import of JSON out of the box, courtesy of the standard implementation of import attributes in Node.

The same code above now becomes:

JavaScript
import jsonData from './foo.json' with { type: 'json' };

No need to import the fs module for reading a file manually or calling on to the JSON.parse() method — it's all baked into Node with the addition of the import attributes standard.

A concrete example for Node.js

Suppose we have the following JSON file, languages.json, as before:

languages.json
JSON
[
   {
      "name": "JavaScript",
      "ext": ".js"
   },
   {
      "name": "Python",
      "ext": ".py"
   },
   {
      "name": "C++",
      "ext": ".cpp"
   }
]

We want to import this as JSON in JavaScript and then log its value (which would obviously be an array).

First, let's consider what happens if we import this using the CommonJS module system.

Below we have a main.cjs file (which is guaranteed to use the CommonJS module system regardless of package.json configuration by virtue of the .cjs extension) with the desired require() call to the JSON file (in the same directory):

main.cjs
JavaScript
const languages = require('./languages.json');

Let's inspect languages now:

main.cjs
JavaScript
const languages = require('./languages.json');

console.log(languages instanceof Array);
console.log(languages);
true
[
   { name: 'JavaScript', ext: '.js' },
   { name: 'Python', ext: '.py' },
   { name: 'C++', ext: '.cpp' }
]

As you can see, the log confirms that the import went well — languages holds an array representing the parsed contents of languages.json.

Now, it's time for the real business: to replicate this behavior in an ECMAScript module.

Following we have a main.mjs file (which is guaranteed to use the ECMAScript module system regardless of package.json configuration by virtue of the .mjs extension) with the desired import statement for importing the JSON file:

main.mjs
JavaScript
import languages from './languages.json' with { type: 'json' };

Notice the added with { type: 'json } syntax. This inclusion of the type import attribute is necessary in order to correctly import the contents of the file as JSON.

In fact, the import attribute is necessary in order to prevent an import error. That is, if you remove the type import attribute, the import will fail, as demonstrated below:

main.mjs
JavaScript
import languages from './languages.json';
node:internal/modules/esm/assert:88
        throw new ERR_IMPORT_ATTRIBUTE_MISSING(url, 'type', validType);
              ^

TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module ".../languages.json" needs an import attribute of "type: json"

The reason for this error is simple: Node recognizes that the imported file has a .json extension and is likewise a JSON file (at least, Node assumes that), therefore, it must be flagged as such with the help of the type import attribute in the code.

Anyways, resuming where we left, let's inspect languages as before:

main.mjs
JavaScript
import languages from './languages.json' with { type: 'json' };

console.log(languages instanceof Array);
console.log(languages);
true
[
   { name: 'JavaScript', ext: '.js' },
   { name: 'Python', ext: '.py' },
   { name: 'C++', ext: '.cpp' }
]

Voila! Our import is working flawlessly!

Further reading

Import attributes are still a relatively recent addition to JavaScript and it may take a while before the whole community feels at home using them.

If you're interested in digging deeper into import attributes, here are some worthwhile resources to check out:

Bilal Adnan

Hi there! 👋 I'm the founder of Codeguage — basically the guy who's trying to make life easy for absolute beginners entering into computer science. You can follow me on LinkedIn or Medium to stay up-to-date with my conversations.