JSON to TypeScript in 300 Lines JS

Staring at raw JSON from a new API? This 300-line vanilla JS beast infers TypeScript types across samples, spots optionals, and spits type guards. No more guesswork.

Inside the 300-Line JSON-to-TypeScript Engine That Outsmarts Quicktype — theAIcatchup

Key Takeaways

  • Tiny AST + refs enable named nests and easy merging
  • Multi-sample optionals emerge naturally from merge logic
  • Dual rendering (interfaces + guards) from one type tree

You’re a dev slamming into a fresh API. JSON dumps everywhere, no schema, just chaos. Hours vanish eyeballing shapes, hammering out TypeScript interfaces — or worse, pasting from bloated web tools that mangle nests and miss optionals.

This JSON-to-TypeScript generator flips that script. In 300 lines of vanilla JS, no deps, browser-ready, it chews multiple samples, merges them smartly, names nested objects, flags optionals. Real people — you, grinding code — reclaim those hours.

And here’s the kick: it unveils TypeScript’s type inference as dead simple, not the black box big tools pretend.

Why Bother When Quicktype Already Rules?

Quicktype? Solid. But it’s a multi-language monster, web app heavy, overkill for “gimme TS from this JSON blob.” The author nails it: “quicktype exists and is excellent — but it’s also a multi-language beast with a web app that’s heavier than I need for this one job.”

So, strip it bare. One job. Vanilla JS. Lives here: demo, GitHub.

Look. This isn’t reinventing wheels — it’s polishing one to a mirror. Exposes the how buried in giants.

The secret sauce? A razor-thin AST. No enums, no literals, no generics. Just primitives, arrays, object refs, unions.

type TsType =
| { kind: 'primitive', name: 'string'|'number'|'boolean'|'null'|'undefined'|'any' }
| { kind: 'array', element: TsType }
| { kind: 'object', ref: string }
| { kind: 'union', types: TsType[] }

Objects? Refs only — no inline blobs. Forces named interfaces for nests. Genius. Keeps merging sane.

How Does Multi-Sample Magic Actually Work?

One function rules them: mergeTypes. Recursive. Handles primitives to unions, arrays by elements, objects by refs.

Paste two samples:

{"id": 1, "name": "A", "email": "a@x"}

{"id": 2, "name": "B", "age": 30}

Boom:

export interface Root { id: number name: string email?: string age?: number }

Emails, ages? Optional. IDs, names? Locked required. No special case. Falls out of merging.

mergeInterfacesInPlace scans keys across interfaces. Missing in one? Optional. Same types? Merge ‘em. Boom, unified.

if (fa && fb) {
  merged.push({ key, type: mergeTypes(fa.type, fb.type, ctx), optional: fa.optional || fb.optional })
} else {
  // Only in one → optional in the merge
  merged.push({ ...(fa || fb), optional: true })
}

That’s the alchemy. Array elements? First one, then merge rest. Mixed? Union. Samples as root array? Same loop. Effortless.

But wait — my unique angle. This echoes 1970s Unix tools. diff merging files without bloat. Type inference stripped like grep regexps. Big tools (quicktype, even TS compiler) pile abstractions; this proves core needs no PhD. Prediction: as AI code-gen flops on edge cases, pocket engines like this explode. Devs hoard control.

Quicktype’s PR? “Excellent multi-lang.” Sure. But hype masks the simple truth: 90% value in 10% code.

Why Does the Type Guard Generation Feel Free?

Same AST. Dual renderers: one TS interfaces, one runtime guards.

export function isRoot(obj: unknown): obj is Root {
  if (typeof obj !== 'object' || obj === null) return false
  // ...
  if (o.email !== undefined && !(typeof o.email === 'string')) return false
  // ...
}

Walks types, emits typeof checks. Optionals? !== undefined &&. Unions? Chain guards.

No duplication. AST centralizes. Change inference, both update. That’s architecture shift: types as data, not strings.

Tradeoffs? No intersections, no deep literals. Fine for APIs — 80% cases. Overkill kills speed.

Runs browser. Copy-paste JSONs, snag code. Dead simple.

What Happens When JSON Gets Messy?

Unions shine. Primitives mismatch? Union. Arrays of mixes? Element unions.

Nested objects auto-name: Root refs User, etc. No anon hell.

Edge: nulls, undefineds handled as primitives. Smart enough.

I’ve tested kin tools. This? Crispest multi-sample. Others union everything, bloat types.

Devs, fork it. Tweak AST, own your inferencer.


🧬 Related Insights

Frequently Asked Questions

What is json-to-ts?

A 300-line vanilla JS tool turning JSON samples into TypeScript interfaces, merging multiples for optionals and unions.

How does JSON to TypeScript merging work?

Recursive mergeTypes function blends types; missing fields turn optional via mergeInterfacesInPlace. Handles arrays, nests, primitives.

Does it generate TypeScript type guards?

Yes, from the same AST — runtime isRoot functions checking typeof and optionals.

Priya Sundaram
Written by

Hardware and infrastructure reporter. Tracks GPU wars, chip design, and the compute economy.

Frequently asked questions

What is json-to-ts?
A 300-line vanilla JS tool turning JSON samples into TypeScript interfaces, merging multiples for optionals and unions.
How does <a href="/tag/json-to-typescript/">JSON to TypeScript</a> merging work?
Recursive `mergeTypes` function blends types; missing fields turn optional via `mergeInterfacesInPlace`. Handles arrays, nests, primitives.
Does it generate TypeScript <a href="/tag/type-guards/">type guards</a>?
Yes, from the same AST — runtime `isRoot` functions checking `typeof` and optionals.

Worth sharing?

Get the best AI stories of the week in your inbox — no noise, no spam.

Originally reported by Dev.to

Stay in the loop

The week's most important stories from theAIcatchup, delivered once a week.