JavaScript Import Attributes Explained
Import attributes let JavaScript modules declare a resource's expected type at import time, such as JSON — catching mismatches before code runs.
Import attributes are a syntax addition to JavaScript’s import statement that let you attach metadata to a module import — most commonly, declaring what type of resource you expect to load. The main use case today is importing JSON directly as a module:
import config from "./config.json" with { type: "json" };
Without the with { type: "json" } clause, a JavaScript engine has no reliable way to know a .json file should be parsed as data rather than executed as script — file extensions aren’t part of the module specifier’s meaning, and a server could serve JSON with the wrong MIME type. Import attributes make that expectation explicit and part of the language.
Why JSON needed its own syntax
JavaScript’s module system was originally built to import other JavaScript. Files like JSON, CSS, or WebAssembly don’t fit that model — they aren’t executable code, so they need a defined way to be turned into a module’s default export.
Before import attributes, loading JSON in a module meant either using fetch() and JSON.parse() at runtime, or relying on a bundler’s non-standard JSON-import support (already common in tools built on the ecosystem around esbuild, Rollup, and Webpack). Both work, but neither is part of the JavaScript specification itself, so behavior varied between environments.
Import attributes standardize this: the type attribute tells the module loader how to interpret the resource before it’s fetched or evaluated, and the loader can reject the import if the actual content doesn’t match — a JSON import that receives a JavaScript file back, for instance, fails instead of silently executing it.
Static and dynamic syntax
The attribute clause works with both static and dynamic imports:
// Static import
import settings from "./settings.json" with { type: "json" };
// Dynamic import
const settings = await import("./settings.json", {
with: { type: "json" },
});
The dynamic form is particularly useful when the module path is computed at runtime — for example, loading a locale file whose name depends on the user’s language setting.
A security motivation, not just convenience
Import attributes exist partly to close a security gap. Without a declared type, a runtime importing a URL has to guess the content type, often from the Content-Type HTTP header or the file extension — both of which a compromised or misconfigured server could get wrong or an attacker could spoof. If a JSON-shaped import silently fell back to executing as JavaScript, that would be an injection vector.
Declaring type: "json" pins the loader to a specific interpretation. If the server or file doesn’t actually deliver JSON, the import fails outright rather than falling back to something more dangerous. This is the same instinct behind explicit content typing elsewhere on the web platform — see what is a Content Security Policy for a related mechanism that constrains what a page is allowed to load and execute.
Import attributes vs import assertions
If you’ve seen assert { type: "json" } in older code or documentation, that’s the predecessor syntax. Import assertions used assert instead of with and worked similarly, but the keyword was changed to with as the proposal evolved, partly because assert implied the check happened after fetching, when the intent was always to inform the fetch itself. Assertion syntax has been phased out in favor of with — new code should use with.
Where this fits with modules generally
Import attributes are additive to the existing module system covered in ESM vs CommonJS — they only change how a specific import is interpreted, not how modules resolve or execute more broadly. They’re most relevant in environments that support native ES modules end to end, including modern browsers and current Node.js releases; in TypeScript codebases, the syntax type-checks the same way and compiles through to the same runtime behavior.
If you already load configuration or data via JSON in a module graph, import attributes let the loader — not your application code — enforce that the resource actually is JSON before your code ever sees it.
The takeaway
Import attributes attach a type declaration to a module import, most usefully with { type: "json" } for pulling in JSON as a first-class module. They replace the older assert syntax, work in both static and dynamic imports, and add a real safety property: the loader verifies the resource matches what you declared instead of quietly executing whatever it receives.
Tagged
Keep reading
Takina · · 4 min read Structural Typing vs Nominal Typing in TypeScript
Structural typing checks shape, not name — TypeScript treats two differently-named types as compatible if their members match, unlike nominal systems.
Takina · · 4 min read Web Streams API Explained: Readable, Writable, Transform
The Streams API lets JavaScript process data as chunks arrive instead of buffering it all in memory. How ReadableStream, WritableStream, and pipes work.
Takina · · 4 min read npm and pnpm Workspaces: Managing a Monorepo
Workspaces let npm and pnpm manage multiple packages in one repository, sharing a single dependency tree and letting packages reference each other locally.