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
- Read more: Linux Kernel’s New Shield Against TPM Interposer Sneak Attacks
- Read more: Hyprland: Tiling Windows That Snap Your Workflow into Hyperdrive
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.