import defer / import attributes
Passes host-specific attributes on an import and defers a module's evaluation until first property access.
import obj from "./data.json" with { type: "json" };
const obj = await import("./data.json", { with: { type: "json" } });
import type { T } from "pkg" with { "resolution-mode": "require" };
type U = import("pkg", { with: { "resolution-mode": "import" } }).T;
import defer * as feature from "./feature.js";- Added in TypeScript 5.3
Parameters
| Parameter | Type | Description |
|---|---|---|
$with { ... } | attribute record | Host-specific key/value pairs passed to the runtime. TypeScript does not check the contents — `type: "json"` and friends are interpreted by the browser or runtime, not the compiler. |
$typeoptional | string | The conventional attribute naming the expected module format, most commonly `"json"`. |
$resolution-modeoptional | "import" | "require" | TypeScript-specific attribute, valid on `import type` and `import()` types, forcing the specifier to be resolved with ESM or CommonJS semantics regardless of the containing file's format. |
$defer | keyword | Placed after `import`, defers evaluation of the module body until a property of the namespace is first read. Only the namespace form `import defer * as ns from "..."` is permitted. |
$* as ns | namespace binding | The only binding form `import defer` accepts. Named and default imports are rejected, because either would require evaluating the module to produce the binding. |
Errors
Raises "Import attributes are only supported when the '--module' option is set to 'esnext', 'nodenext', or 'preserve'", and for the deferred form "Deferred imports are only supported when the '--module' option is set to 'esnext' or 'preserve'"; a named or default binding after `defer` is also an error.
Changelog
- PHP 4.5Import assertions (`assert { type: "json" }`) supported, following the earlier TC39 proposal.
- PHP 5.3Import attributes (`with { ... }`) supported, superseding assertions; `resolution-mode` on `import type` becomes stable and works under every `moduleResolution` mode including `bundler` and `node10`.
- PHP 5.9`import defer * as ns from "..."` added, limited to `--module preserve` and `esnext` and never downleveled.
Two newer pieces of module syntax that TypeScript types but does not implement.
Import attributes
with { type: "json" } passes information to the runtime about the format a specifier is expected to have. TypeScript deliberately does not validate the contents — the keys and values are host-specific, so the compiler parses them, type-checks the import itself, and leaves the attributes alone. They replaced the older assert { ... } syntax; the assertion spelling still parses but the proposal moved on, so write with.
The one attribute TypeScript itself understands is resolution-mode, which is valid only on import type and import() types. It forces a specifier to resolve under ESM or CommonJS rules regardless of the containing file — the way to reach both halves of a dual-published package's types from one file.
import defer
import defer * as feature from "./feature.js" loads and links the module but does not run its body until you first read a property off the namespace. Use it for a module with expensive or platform-specific initialisation that most code paths never touch.
What trips people up
Neither feature is downleveled. Both need --module esnext or preserve (attributes also allow nodenext) and a runtime or bundler that implements them.
Example
// tsconfig: { "module": "esnext", "resolveJsonModule": true }
import config from "./config.json" with { type: "json" };
import defer * as report from "./report.js"; // body not run yet
const dynamic = await import("./config.json", { with: { type: "json" } });
console.log(config.name, dynamic.default.name);
// -> "soundscode" "soundscode"
console.log(report.render(config.name)); // first access: report.js evaluates now
// -> "Side effects have occurred!"
// -> "report for soundscode"When to use it
- Import a JSON module in a browser or Node ESM context that requires the type attribute
- Skip expensive initialisation of a rarely-used feature module until it is first touched
- Pull types from both the ESM and CommonJS halves of a dual-published package in one file
- Get a type from a CommonJS-only package inside an ES module file without changing resolution settings
Frequently asked questions
Why do I get "Import attributes are only supported when module is set to esnext, nodenext, or preserve"?
Because attributes cannot be downleveled — there is no CommonJS equivalent of `with { type: "json" }`. Set `"module"` to `esnext`, `nodenext` or `preserve` in tsconfig, or remove the attribute if your build target is CommonJS.
What is the difference between import defer and a dynamic import?
`import defer` loads and links the module eagerly but delays running its top-level code until you read a property of the namespace, and stays synchronous. `await import(...)` defers the fetch as well and returns a promise, so the call site must be async. Reach for `import defer` when the cost is initialisation rather than loading.
Does TypeScript check import attribute values?
No. Attributes are host-specific, so TypeScript parses them and passes them through untouched — a misspelled `type: "jsonn"` compiles fine and fails at runtime. The one exception is `resolution-mode`, which TypeScript defines itself and accepts only the values `"import"` and `"require"` on type-only imports.
More examples
When import defer actually runs the module
The ordering in the output is the whole point: "app: started" prints before "heavy-feature: evaluating", and if the flag is absent the second line never prints at all. The module is fetched and linked eagerly — only the execution of its top-level statements waits. That is the difference from `await import(...)`, which defers loading too and forces the call site to be async.
// heavy-feature.ts
console.log("heavy-feature: evaluating");
const table = buildLookupTable();
function buildLookupTable(): Map<string, number> {
return new Map([["a", 1], ["b", 2]]);
}
export function lookup(k: string): number | undefined { return table.get(k); }
// app.ts (--module esnext or preserve)
import defer * as heavy from "./heavy-feature.js";
console.log("app: started");
// -> "app: started" (heavy-feature has NOT logged yet)
if (process.argv.includes("--with-lookup")) {
console.log(heavy.lookup("b"));
// -> "heavy-feature: evaluating"
// -> 2
}resolution-mode: reading both halves of a dual package
Both imports name the same specifier and get different declaration files, because `resolution-mode` overrides the resolution semantics the containing file would normally impose. This is how you write a shim that must satisfy consumers of both entry points. Note the attributes are on `import type` only — the resolution mode is a compile-time concern and there is no emitted import to carry it.
// A package that ships different types for its ESM and CJS entry points.
import type { ClientOptions as EsmOptions } from "dual-pkg" with {
"resolution-mode": "import"
};
import type { ClientOptions as CjsOptions } from "dual-pkg" with {
"resolution-mode": "require"
};
// Also available in import() type position:
type LegacyClient = import("dual-pkg", {
with: { "resolution-mode": "require" }
}).Client;
export interface MergedOptions extends EsmOptions, CjsOptions {}
const opts: MergedOptions = { endpoint: "https://example.test", retries: 2 };
console.log(Object.keys(opts).sort());
// -> ["endpoint", "retries"]
declare const legacy: LegacyClient;
Discussion