Learn Advanced JavaScript in about a week!
Learning should be fun and quick; no need to spend ages in just one single course.
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.
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'
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):
// 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:
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:
let response = await fetch('./foo.json');
let jsonData = await response.json();
// Work with jsonDataEither 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:
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.
with {} syntax.A concrete example for the browser
Suppose we have the following HTML document:
<!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:
[
{
"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:
import languages from './languages.json' with { type: 'json' };Let's inspect languages and see what it contains:
import languages from './languages.json' with { type: 'json' };
console.log(languages instanceof Array);
console.log(languages);
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:
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():
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:
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:
[
{
"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):
const languages = require('./languages.json');Let's inspect languages now:
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:
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:
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:
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:
- Import attributes | MDN (Could any "further reading" list be complete without a mention of MDN?)
- tc39/proposal-import-attributes
- Import attributes | V8.dev
- Import attributes — ECMAScript modules | Node.js