JSON → TypeScript — JavaScript source
Paste any JSON and instantly get clean, typed TypeScript interfaces - primitives, nested objects, arrays and unions, all inferred. Optional keys, reserved-word quoting, and shape dedup are handled for you. Runs 100% in your browser.
This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
// =============================================================================
// json-to-typescript - JavaScript port
// =============================================================================
// Infer a TypeScript interface tree from any JSON-serializable value.
//
// CosmoDev polyglot showcase port of the `json-to-typescript` tool.
// Ported from src/lib/json-to-typescript.ts (the canonical TypeScript lib).
//
// Pure, deterministic, zero deps. Object values become named interfaces
// (deduplicated by structural shape); arrays become `T[]`; primitives map to
// TS primitives; literal `null` becomes `null`. See `jsonToTs`.
//
// This is display source - part of CosmoDev's polyglot tool pages.
// =============================================================================
// TypeScript reserved words + built-in type names. Any object key that appears
// here (or isn't a bareword identifier) must be emitted as a quoted string key.
const RESERVED = new Set([
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default',
'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for',
'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super',
'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with',
'as', 'async', 'await', 'yield', 'let', 'static', 'implements', 'interface',
'package', 'private', 'protected', 'public', 'type', 'readonly', 'namespace',
'abstract', 'any', 'boolean', 'never', 'number', 'object', 'string', 'symbol',
'undefined', 'unknown', 'keyof', 'infer', 'satisfies',
]);
// ---- Type tree -------------------------------------------------------------
// A discriminated union describing a TypeScript type. `signature` is a structural
// fingerprint used to dedupe identical object shapes independent of their names.
function primitive(ts) {
return { kind: 'primitive', ts };
}
const UNKNOWN = { kind: 'unknown' };
const NULL_NODE = { kind: 'primitive', ts: 'null' };
function newObject(props, nameHint) {
const node = { kind: 'object', props, signature: '', nameHint };
node.signature = signatureOf(node);
return node;
}
function newUnion(members) {
const node = { kind: 'union', members, signature: '' };
node.signature = signatureOf(node);
return node;
}
// ---- Small text helpers ----------------------------------------------------
// PascalCase a key segment for use in an interface name (`user_id` -> `UserId`).
// Empty input collapses to `Item`; a leading digit is escaped with `N`.
function pascal(key) {
const parts = String(key).split(/[^A-Za-z0-9]+/).filter(Boolean);
const head = parts.length
? parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('')
: 'Item';
return /^[0-9]/.test(head) ? `N${head}` : head;
}
// Singularize an interface name for array-element naming (`Items` -> `Item`).
// We only trim a trailing `s` (never `ss`); otherwise append `Item`.
function singularize(name) {
if (name.length > 1 && name.endsWith('s') && !name.endsWith('ss')) {
return name.slice(0, -1);
}
return `${name}Item`;
}
// Coerce a user-supplied root name into a valid TS identifier (PascalCase).
function sanitizeRoot(name) {
const cleaned = pascal(name);
return cleaned || 'Root';
}
// ---- Input validation ------------------------------------------------------
// Throw on values that cannot round-trip through JSON: functions, symbols,
// undefined, and non-plain objects (class instances, Map, Date, ...).
function assertJsonSerializable(value, path) {
if (value === null || typeof value !== 'object') {
if (typeof value === 'function' || typeof value === 'symbol' || typeof value === 'undefined') {
throw new Error(`Value at ${path || 'root'} is not JSON-serializable (${typeof value})`);
}
return; // string | number | boolean - all fine
}
if (Array.isArray(value)) {
value.forEach((v, i) => assertJsonSerializable(v, `${path}[${i}]`));
return;
}
// Only `{}`-created objects (or null-proto) count as plain JSON objects.
const proto = Object.getPrototypeOf(value);
if (proto === null || proto === Object.prototype) {
for (const k of Object.keys(value)) {
assertJsonSerializable(value[k], `${path}.${k}`);
}
return;
}
throw new Error(
`Value at ${path || 'root'} is not a plain JSON object (${proto?.constructor?.name ?? 'object'})`,
);
}
// ---- Structural signature & shape helpers ----------------------------------
// A string fingerprint of a type's shape, independent of assigned names. Object
// signatures include each key (with optionality) and recurse, so two objects
// with the same shape always share a signature.
function signatureOf(node) {
switch (node.kind) {
case 'primitive':
return node.ts;
case 'unknown':
return '?';
case 'array':
return node.of ? `[${signatureOf(node.of)}]` : '[]';
case 'union':
return `(${node.members.map(signatureOf).join('|')})`;
case 'object': {
const body = node.props
.map((p) => `${p.key}${p.optional ? '?' : ''}:${signatureOf(p.type)}`)
.join(';');
return `{${body}}`;
}
}
}
// Does a type contain a `null` leaf? Used by `optionalNullable`.
function containsNull(node) {
if (node.kind === 'primitive') return node.ts === 'null';
if (node.kind === 'union') return node.members.some(containsNull);
return false;
}
// Dedupe nodes by structural signature, preserving first-seen order.
function dedupe(nodes) {
const seen = new Set();
const out = [];
for (const n of nodes) {
const s = signatureOf(n);
if (!seen.has(s)) {
seen.add(s);
out.push(n);
}
}
return out;
}
// ---- Combining array element types -----------------------------------------
// Merge several object nodes into one: union of keys (keys absent from some
// element become optional), recursing per key.
function mergeObjects(objs, opts) {
const keyOrder = [];
const byKey = new Map();
for (const o of objs) {
for (const p of o.props) {
if (!byKey.has(p.key)) {
keyOrder.push(p.key);
byKey.set(p.key, []);
}
byKey.get(p.key).push(p.type);
}
}
const props = keyOrder.map((key) => {
const childTypes = byKey.get(key);
const type = combine(childTypes, opts);
let optional = childTypes.length < objs.length; // missing from some element
if (opts.optionalNullable && containsNull(type)) optional = true;
return { key, type, optional };
});
return newObject(props, objs[0].nameHint);
}
// Combine a list of element types into one.
// - empty -> unknown
// - unionArrays -> distinct union of every member
// - else -> merge where sensible (objects merge keys; distinct primitives
// union), and build a union only for genuinely heterogeneous input
function combine(nodes, opts) {
if (nodes.length === 0) return UNKNOWN;
if (opts.unionArrays) {
const d = dedupe(nodes);
return d.length === 1 ? d[0] : newUnion(d);
}
const objs = nodes.filter((n) => n.kind === 'object');
const arrs = nodes.filter((n) => n.kind === 'array');
const prims = dedupe(nodes.filter((n) => n.kind === 'primitive'));
const hasUnknown = nodes.some((n) => n.kind === 'unknown');
// Pure primitive/unknown arrays collapse: identical -> single, distinct -> union.
if (objs.length === 0 && arrs.length === 0) {
const members = [...prims];
if (hasUnknown) members.push(UNKNOWN);
const d = dedupe(members);
return d.length === 1 ? d[0] : newUnion(d);
}
// Homogeneous object array -> a single merged object.
if (objs.length > 0 && arrs.length === 0 && prims.length === 0 && !hasUnknown) {
return mergeObjects(objs, opts);
}
// Otherwise build a union of the meaningful parts.
const members = [];
if (objs.length) members.push(mergeObjects(objs, opts));
if (arrs.length) {
const ofTypes = arrs.map((a) => a.of ?? UNKNOWN);
members.push({ kind: 'array', of: ofTypes.length ? combine(ofTypes, opts) : null });
}
members.push(...prims);
if (hasUnknown) members.push(UNKNOWN);
const d = dedupe(members);
return d.length === 1 ? d[0] : newUnion(d);
}
// ---- Inference -------------------------------------------------------------
// Recursively infer a type tree from a JSON value. `hint` is the interface
// name to use if this value is an object.
function infer(value, hint, opts) {
if (value === null) return NULL_NODE;
const t = typeof value;
if (t === 'string') return primitive('string');
if (t === 'number') return primitive('number');
if (t === 'boolean') return primitive('boolean');
if (Array.isArray(value)) {
if (value.length === 0) return { kind: 'array', of: null };
const elemHint = singularize(hint);
const elements = value.map((e) => infer(e, elemHint, opts));
return { kind: 'array', of: combine(elements, opts) };
}
// plain object (serializability already asserted upstream)
const props = Object.keys(value).map((key) => {
const type = infer(value[key], `${hint}${pascal(key)}`, opts);
let optional = false;
if (opts.optionalNullable && containsNull(type)) optional = true;
return { key, type, optional };
});
return newObject(props, hint);
}
// ---- Rendering -------------------------------------------------------------
function renderKey(key) {
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) && !RESERVED.has(key)) return key;
return JSON.stringify(key);
}
// Wrap an array element in parens if it would otherwise mis-parse (a union).
function wrapArray(rendered, of) {
return of.kind === 'union' ? `(${rendered})` : rendered;
}
// Render a type node to its TS string form.
function renderType(node, names) {
switch (node.kind) {
case 'primitive':
return node.ts;
case 'unknown':
return 'unknown';
case 'object':
return names.get(node.signature) ?? 'unknown';
case 'array':
return node.of ? `${wrapArray(renderType(node.of, names), node.of)}[]` : 'unknown[]';
case 'union':
return dedupe(node.members).map((m) => renderType(m, names)).join(' | ');
}
}
// Collect every object node (deduped by shape) in first-seen order, naming each.
// Names are unique: when two distinct shapes share a path-derived hint, later
// ones get a numeric suffix. Only the actual root node takes `rootName`; an
// array-of-objects root therefore names its element `<Root>Item`.
function collectObjects(root, rootName) {
const names = new Map();
const usedNames = new Set();
const order = [];
const visit = (node, isRoot) => {
if (node.kind === 'object') {
if (!names.has(node.signature)) {
let candidate = isRoot ? rootName : node.nameHint;
if (usedNames.has(candidate)) {
let i = 2;
while (usedNames.has(`${candidate}${i}`)) i++;
candidate = `${candidate}${i}`;
}
names.set(node.signature, candidate);
usedNames.add(candidate);
order.push(node);
for (const p of node.props) visit(p.type, false);
} else {
// already named - still recurse to discover newly-seen nested shapes
for (const p of node.props) visit(p.type, false);
}
} else if (node.kind === 'array' && node.of) {
visit(node.of, false);
} else if (node.kind === 'union') {
for (const m of node.members) visit(m, false);
}
};
visit(root, true);
return { order, names };
}
function renderInterface(o, names) {
const name = names.get(o.signature);
if (o.props.length === 0) return `interface ${name} {}`;
const lines = o.props.map(
(p) => ` ${renderKey(p.key)}${p.optional ? '?' : ''}: ${renderType(p.type, names)};`,
);
return `interface ${name} {\n${lines.join('\n')}\n}`;
}
// ---- Public entry point -----------------------------------------------------
/**
* Infer TypeScript interfaces from any JSON-serializable value.
*
* @example
* jsonToTs({ name: 'a', age: 1 })
* // interface Root {\n name: string;\n age: number;\n}
*/
export function jsonToTs(value, opts = {}) {
const resolved = {
rootName: sanitizeRoot(opts.rootName ?? 'Root'),
unionArrays: opts.unionArrays ?? false,
optionalNullable: opts.optionalNullable ?? false,
};
assertJsonSerializable(value, '');
const root = infer(value, resolved.rootName, resolved);
// Primitive / unknown / array roots emit a `type` alias; object roots emit interfaces.
if (root.kind !== 'object') {
if (root.kind === 'array') {
const { order, names } = collectObjects(root, resolved.rootName);
const ifaces = order.map((o) => renderInterface(o, names)).join('\n\n');
const alias = `type ${resolved.rootName} = ${renderType(root, names)};`;
return ifaces ? `${ifaces}\n\n${alias}` : alias;
}
return `type ${resolved.rootName} = ${renderType(root, new Map())};`;
}
const { order, names } = collectObjects(root, resolved.rootName);
return order.map((o) => renderInterface(o, names)).join('\n\n');
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →