The short answer
Import the file with a default import and the with { type: "json" } attribute. tsc types the value from the JSON itself:
import catalog from "./catalog.json" with { type: "json" };
/** @param {string} sku */
export function priceOf(sku) {
return catalog.products.find((product) => product.sku === sku)?.cents;
}
console.log(catalog.currency.toLowerCase(), priceOf("LAMP-01"));import catalog from "./catalog.json" with { type: "json" };
export function priceOf(sku: string) {
return catalog.products.find((product) => product.sku === sku)?.cents;
}
console.log(catalog.currency.toLowerCase(), priceOf("LAMP-01"));jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} catalog.currency is a string, catalog.products an array of { sku, name, cents } objects, and a lookup returns number | undefined, all from the file.
Why it works
Node.js requires the type: "json" attribute when an ES module imports JSON: the importing code states what kind of module it expects, and a file that turns out to be something else is refused. TypeScript follows Node.js under module: "nodenext", reads the JSON file at check time, and infers its type the way it would infer an object literal. Under nodenext in TypeScript 5.9, JSON resolution (resolveJsonModule) is on by default.
The same thing in TypeScript
The TypeScript equivalent tab has the same import line; import attributes are JavaScript syntax, and TypeScript 5.3 and later checks them. Nothing in the JSON needs a type annotation in either language.
What goes wrong
Leave out the attribute and tsc reports what Node.js would refuse (TS1543); ask for a named export and it reports that JSON modules have only a default export (TS1544); a misspelled field is TS2551:
import catalog from "./catalog.json";error TS1543: Importing a JSON file into an ECMAScript module requires a 'type: "json"' import attribute when 'module' is set to 'NodeNext'.
import { currency } from "./catalog.json" with { type: "json" };error TS1544: Named imports from a JSON file into an ECMAScript module are not allowed when 'module' is set to 'NodeNext'.
console.log(catalog.product.length, currency);error TS2551: Property 'product' does not exist on type '{ currency: string; products: { sku: string; cents: number; }[]; }'. Did you mean 'products'?jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Gotchas
- Values are widened. The type comes from the file’s content, so
"USD"is typedstring, not the literal"USD"; the JSON is data, and data can change. - No
includeentry is needed. tsc reads the JSON through the import;includelists the files that start the program, not every file it reaches. assertis the old keyword.import catalog from "./catalog.json" assert { type: "json" }is TS2880 in TypeScript 5.9, which asks forwithinstead.
Questions
Do I need resolveJsonModule in jsconfig.json?
Not with module: "nodenext": TypeScript 5.9 turns it on by itself there (and under node20, preserve and bundler). With node16 or node18, or an older module setting, set "resolveJsonModule": true.
What about require("./catalog.json") in CommonJS?
It works without an import attribute, and tsc types the result from the file the same way, as long as JSON resolution is on.
Can I read the JSON with fs instead?
Yes, but then the result of JSON.parse is any; see typing JSON.parse. An import is checked against the actual file.
Sources
- TypeScript 5.3 release notes (import attributes) · TypeScript handbook
- Modules (JSON modules) · Node.js documentation
- TSConfig Reference (resolveJsonModule) · TypeScript
Last reviewed 21 October 2026.