JSON6 Guide for TypeScript Developers

The json-6 package ships its own declaration files. Here is what they say, how to type parse() results, and how to validate untrusted data.

TL;DR: No @types package is needed: json-6 1.1.7 ships declarations in dist/. JSON6.parse() returns unknown, so narrow the result with a type guard or validate it with a schema library such as zod instead of casting. Use a default import: import JSON6 from 'json-6'.

What the Declaration Files Contain

The package builds its TypeScript declarations from JSDoc comments in the parser source, which landed in release 1.1.5, and ships them in the dist/ folder. The exports map in package.json points each import style at the right file, so the compiler resolves them without help. Nothing in the package uses the legacy top-level types field.

Import styleDeclaration fileShape
import JSON6 from 'json-6' (ESM)dist/index-esm.d.mtsRe-exports the default value from dist/index.d.ts.
require('json-6') or import JSON6 = require(...) (CJS)dist/json6.d.tsexport = an object, merged with a namespace that holds the exported types.
require('json-6/lib/require')dist/require.d.tsExports unknown. It is loaded for its side effect.
--import json-6/lib/register.mjsdist/register.d.mtsAn empty module (export {}).

The public API surface in the declarations is small and matches the JavaScript guide one to one. parse takes (msg: unknown, reviver?, options?) and returns unknown. stringify takes (object: unknown, replacer?, space?: string | number, options?) and returns string | undefined, because stringifying undefined gives undefined. begin takes an optional callback (value: unknown) => void and returns the parser object. The remaining members are escape, stringifier, stringifierActive and version: string.

These are the supporting types, copied from the shipped dist/json6.d.ts (the file defines them inside the JSON6_1 namespace, hence the indentation).

type Json6Reviver = (key: string, value: unknown) => unknown; type Json6Replacer = ((key: string, value: unknown) => unknown) | Array<string | number> | null; type Json6StringifyOptions = { sortKeys?: boolean; };

The parse options type is Json6ParseOptions with four optional booleans: esStrictCompatible, forbidTemplateSubstitution, warnWithCommentWithoutEOL and normalizeNumericKeys. The stream parser returned by begin() has write(msg?: unknown), value(): unknown, reset(resetOptions?), the low-level _write(msg?, complete_at_end?) returning a number, and a finalError() method that the README does not document. Treat _write and finalError as internal.

Release 1.1.5 introduced the declarations and release 1.1.6 fixed a real defect in them: the CommonJS file mixed export = with export type lines, which TypeScript 5.x rejects (error TS2309) for any project that compiles without skipLibCheck. If you are on 1.1.5, upgrade to 1.1.6 or later. All checks on this page used 1.1.7 with skipLibCheck off.

Compiler Setup

Use a module resolution mode that understands the exports field: "module": "nodenext" (which implies moduleResolution: nodenext) for Node projects and "moduleResolution": "bundler" for code that goes through Vite, esbuild or similar. I compiled the examples below with TypeScript 7.0.2 under both settings with strict on and skipLibCheck off, and the result was zero errors. Under nodenext it works from both an ESM package ("type": "module") and a CommonJS one, and the default import type-checks in both.

{ "compilerOptions": { "module": "nodenext", "target": "es2022", "strict": true, "noEmit": true } }

Two cautions. First, moduleResolution: node10 (the old node setting) ignores the exports map and falls back to main, and this package has no top-level types field. TypeScript 7 removed node10 altogether (the compiler reports that the option "has been removed"), so I could not test it; if you are on an older compiler with the legacy setting, expect the module to be untyped. Switch to nodenext or bundler. Second, tsconfig.json itself is read by TypeScript as JSON with comments. It is not parsed by JSON6, so you cannot point --project at a .json6 file.

Typing parse() Results

Because the parser cannot know what you wrote, parse returns unknown. That is the honest type, and it means TypeScript refuses to let you read a property until you have proved something about the value. Reading value.a straight off the result fails with error TS18046, "'value' is of type 'unknown'". The example below shows the three ways to proceed. They differ in how much they trust the input.

ts-unknown.ts

import JSON6 from 'json-6'; interface Config { name: string; port: number; } const value = JSON6.parse("{ name: 'demo', port: 8080 }"); // 1. A type assertion. Compiles, checks nothing at runtime. Fine for files you control. const trusted = value as Config; console.log(trusted.name, trusted.port); // 2. A type guard. Checks the shape you rely on, then narrows. function isConfig(v: unknown): v is Config { return ( typeof v === 'object' && v !== null && typeof (v as Record<string, unknown>).name === 'string' && typeof (v as Record<string, unknown>).port === 'number' ); } if (isConfig(value)) { console.log('guard ok:', value.port + 1); } // 3. A generic loader that refuses to return anything the guard rejects. function parseAs<T>(text: string, guard: (v: unknown) => v is T): T { const parsed = JSON6.parse(text); if (!guard(parsed)) throw new TypeError('Unexpected shape'); return parsed; } console.log(parseAs("{ name: 'x', port: 1 }", isConfig)); try { parseAs("{ name: 'x', port: '1' }", isConfig); } catch (err) { console.log((err as Error).message); }

Run it:

node ts-unknown.ts

Output:

demo 8080 guard ok: 8081 { name: 'x', port: 1 } Unexpected shape

The assertion is the one most code uses and it is fine for a file that ships inside your own repo. The moment the text comes from a user, a network request or a file that people edit by hand, prefer the guard or a schema. JSON6 makes this more important than plain JSON, for a specific reason: its grammar produces values JSON never can. A document can contain undefined, array holes, Infinity and NaN, and your interface may say number where the file says NaN. An assertion will not notice.

ts-surprises.ts

import JSON6 from 'json-6'; // Valid JSON6, but not valid JSON and not what an interface usually promises. const value = JSON6.parse('{ ratio: NaN, missing: undefined, list: [1,,3], big: Infinity }') as { ratio: number; missing: string; list: number[]; big: number; }; console.log(Number.isNaN(value.ratio)); // true, though the type says number console.log(typeof value.missing); // 'undefined', though the type says string console.log(value.list.length, 1 in value.list); // 3 false: a hole, not a value console.log(Number.isFinite(value.big)); // false

Run it:

node ts-surprises.ts

Output:

true undefined 3 false false

Validate with zod

A schema library gives you the type and the runtime check from one definition. The example uses zod 4 (zod.dev), but the pattern is the same for any library that takes an unknown. Parse with JSON6 first, then hand the result to the schema. Note how the hand-written hex port and the trailing commas are fine to JSON6, and the schema is what rejects a negative, fractional port.

ts-zod.ts

import JSON6 from 'json-6'; import { z } from 'zod'; const ConfigSchema = z.object({ name: z.string(), port: z.number().int().positive(), debug: z.boolean().default(false), tags: z.array(z.string()).default([]), }); type Config = z.infer<typeof ConfigSchema>; function loadConfig(text: string): Config { const raw: unknown = JSON6.parse(text); return ConfigSchema.parse(raw); } const good = loadConfig(`{ name: 'demo', port: 0x1F90, // 8080 tags: ['a', 'b',], }`); console.log(good); // Use safeParse to report problems without throwing. const bad = ConfigSchema.safeParse(JSON6.parse("{ name: 'demo', port: -1.5 }")); if (!bad.success) { for (const issue of bad.error.issues) { console.log(issue.path.join('.'), '-', issue.message); } } // An array hole becomes undefined, which the schema rejects with a path. const holey = ConfigSchema.safeParse(JSON6.parse("{ name: 'x', port: 1, tags: [,] }")); console.log(holey.success ? holey.data : holey.error.issues.map((i) => i.path.join('.') + ': ' + i.message));

Run it:

node ts-zod.ts

Output:

{ name: 'demo', port: 8080, debug: false, tags: [ 'a', 'b' ] } port - Invalid input: expected int, received number [ 'tags.0: Invalid input: expected string, received undefined' ]

Schemas also solve the undefined problem for you. A property written as undefined counts as missing, so .default() and .optional() behave naturally, which is a neat way to use the undefined keyword as "unset" in hand-written config.

Reusing the Package Types

How you name the package's types depends on how you import it. In a CommonJS or import = require style the types are exported inside a namespace on the value, so you can write JSON6.Json6ParseOptions. With the ES module default import there is no merged namespace, because the ESM declaration only re-exports a value. In that case derive the types from the functions with Parameters, which always works.

ts-types.ts

import JSON6 from 'json-6'; type ParseOptions = NonNullable<Parameters<typeof JSON6.parse>[2]>; type Reviver = NonNullable<Parameters<typeof JSON6.parse>[1]>; type Replacer = Parameters<typeof JSON6.stringify>[1]; type StringifyOptions = NonNullable<Parameters<typeof JSON6.stringify>[3]>; const strict: ParseOptions = { esStrictCompatible: true, forbidTemplateSubstitution: true }; const dates: Reviver = (key, value) => (key === 'at' && typeof value === 'string' ? new Date(value) : value); const hideSecrets: Replacer = (key, value) => (key === 'secret' ? undefined : value); const keepOrder: StringifyOptions = { sortKeys: false }; const parsed = JSON6.parse("{ at: '2026-10-27T00:00:00Z', secret: 1 }", dates, strict) as { at: Date }; console.log(parsed.at.toISOString()); // stringify returns string | undefined, so handle the undefined case. const text: string = JSON6.stringify({ b: 1, a: 2, secret: 3 }, hideSecrets, undefined, keepOrder) ?? ''; console.log(text);

Run it:

node ts-types.ts

Output:

2026-10-27T00:00:00.000Z {b:1,a:2}

For the CommonJS style, the same types are directly available. This listing was type-checked with TypeScript 7.0.2 in a CommonJS package under nodenext; it is not executed because import = require is not erasable syntax and Node's type stripping cannot run it.

import JSON6 = require('json-6'); const options: JSON6.Json6ParseOptions = { esStrictCompatible: true }; const reviver: JSON6.Json6Reviver = (key, value) => value; const value: unknown = JSON6.parse('{ a: 1 }', reviver, options); const text: string | undefined = JSON6.stringify(value);

The stream parser is typed too. The callback receives unknown and the object returned by begin() exposes write, reset and value. See the streaming guide for runtime behavior.

Importing .json6 Files with Types

If you load files with the ESM register hook, TypeScript needs to be told what an import of a .json6 path looks like, or it reports that it cannot find a declaration for the module. A wildcard module declaration fixes it. Put it in a .d.ts file that your tsconfig.json includes. This declaration gives every imported file the type unknown, which is the honest answer and forces you to narrow.

// json6.d.ts declare module '*.json6' { const value: unknown; export default value; }

To get a real type for one file, validate it at the import site instead of giving every file a type. The runtime code below imports a file with the register hook and checks it with the same guard idea as above.

ts-import.ts

import raw from './settings.json6'; interface Settings { title: string; retries: number; } function isSettings(v: unknown): v is Settings { const r = v as Record<string, unknown> | null; return typeof r === 'object' && r !== null && typeof r.title === 'string' && typeof r.retries === 'number'; } if (!isSettings(raw)) throw new TypeError('settings.json6 has the wrong shape'); console.log(raw.title, raw.retries);

Run it:

node --import json-6/lib/register.mjs ts-import.ts

Output:

Demo 3

Under nodenext a relative import of an extensionless path or an unknown extension still needs the declaration above, and the compiler does not check that the file exists or what it contains. That is the cost of any custom extension: the type system sees a path, not a file.

Running TypeScript Directly

All of the .ts examples on this page were run with plain node file.ts on Node 24, which erases types before running. There is no build step and no tsx. That only works for erasable syntax: no enum, no parameter properties, no import = require, and the file must use real extensions in relative imports. Node does not type-check, so run tsc --noEmit separately. I type-checked every runnable example on this page with TypeScript 7.0.2 and strict on.

For older Node versions, or if you want decorators and enums, compile with tsc or run through a runner such as tsx. The JSON6 side does not change: it is ordinary JavaScript at runtime. For builds that bundle code for the browser, see the browser guide, which shows an esbuild plugin that imports .json6 files.

Do not use as any to silence the unknown result. It removes exactly the protection that makes the type honest, and with JSON6 the extra syntax means more values that do not match your interface. Use a guard, satisfies on a literal you wrote yourself, or a schema.

FAQ

Does json-6 include TypeScript types?

Yes. Declaration files ship in dist/ since release 1.1.5, and they are wired through the exports map. You do not need a separate @types package, and the npm registry has no @types/json-6 or @types/json6 package.

Why does parse return unknown instead of any?

Because unknown forces you to check the value before using it. The parser cannot know the shape of your file, and an any return would turn off checking for everything derived from it.

Can I get type inference from a .json6 file like resolveJsonModule gives for .json?

No. resolveJsonModule is specific to .json files and the compiler does not understand JSON6 syntax. Declare the module as unknown and validate it, or convert the file to JSON at build time and import the JSON.

Why does import { parse } from 'json-6' fail to compile?

The ES module declaration exports only a default value. TypeScript reports that the module has no exported member named parse, and Node fails the same import at runtime. Import the default and call JSON6.parse.

Which compiler options matter?

Use module: nodenext or moduleResolution: bundler so the exports map is honored. The legacy node10 resolution cannot find the types because the package has no top-level types field, and TypeScript 7 no longer supports that mode.

Should I validate with zod, Valibot or a hand-written guard?

Any works, because they all accept unknown. Use a guard for a handful of keys and a schema library when you have nested objects, defaults or need good error paths. The example above uses zod 4, the one I tested.

Type Your JSON6 Config

Start from the JavaScript reference or jump to the config tutorial.