JSON → TypeScript — Rust 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 Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.
// =============================================================================
// json-to-typescript — Rust 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, stdlib only. Object values become named interfaces
// (deduplicated by structural shape); arrays become `T[]`; primitives map to
// TS primitives; literal `null` becomes `null`. See `json_to_ts`.
//
// This is display source — part of CosmoDev's polyglot tool pages.
// =============================================================================
use std::collections::{HashMap, HashSet};
// ---------------------------------------------------------------------------
// Options (mirror the TS lib exactly)
// ---------------------------------------------------------------------------
#[derive(Clone)]
pub struct Options {
/// Name of the root interface/type. Default `"Root"`.
pub root_name: String,
/// When true, emit a true union of distinct array element types instead of
/// merging them.
pub union_arrays: bool,
/// When true, object properties whose type includes `null` become optional.
pub optional_nullable: bool,
}
impl Default for Options {
fn default() -> Self {
Options {
root_name: "Root".to_string(),
union_arrays: false,
optional_nullable: false,
}
}
}
fn resolve(opts: &Options) -> Options {
let root_name = if opts.root_name.is_empty() {
"Root"
} else {
&opts.root_name
};
Options {
root_name: sanitize_root(root_name),
union_arrays: opts.union_arrays,
optional_nullable: opts.optional_nullable,
}
}
// ---------------------------------------------------------------------------
// JSON value tree (the input)
// ---------------------------------------------------------------------------
// Rust's stdlib has no JSON support, so this file is self-contained: a tiny
// `Value` enum plus a minimal parser. `Object` keeps its pairs in a `Vec` so
// key order — which is semantically observable in the emitted interface — is
// preserved.
#[derive(Clone, Debug)]
pub enum Value {
Null,
Bool(bool),
// JSON numbers map to TS `number` regardless of integral-ness.
Number(f64),
Str(String),
Array(Vec<Value>),
Object(Vec<(String, Value)>),
}
// ---------------------------------------------------------------------------
// Minimal JSON parser
// ---------------------------------------------------------------------------
// Compact recursive-descent parser. Sufficient for any RFC 8259 document a
// caller is likely to feed this tool.
type PResult<T> = Result<T, String>;
struct Parser<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Parser {
bytes: input.as_bytes(),
pos: 0,
}
}
fn skip_ws(&mut self) {
while self.pos < self.bytes.len() {
match self.bytes[self.pos] {
b' ' | b'\t' | b'\n' | b'\r' => self.pos += 1,
_ => break,
}
}
}
fn peek(&self) -> Option<u8> {
self.bytes.get(self.pos).copied()
}
fn parse_value(&mut self) -> PResult<Value> {
self.skip_ws();
match self.peek().ok_or_else(|| "unexpected end of input".to_string())? {
b'{' => self.parse_object(),
b'[' => self.parse_array(),
b'"' => Ok(Value::Str(self.parse_string()?)),
b't' | b'f' => self.parse_bool(),
b'n' => self.parse_null(),
b'-' | b'0'..=b'9' => self.parse_number(),
c => Err(format!("unexpected character {:?}", c as char)),
}
}
fn parse_object(&mut self) -> PResult<Value> {
self.pos += 1; // {
let mut pairs = Vec::new();
self.skip_ws();
if self.peek() == Some(b'}') {
self.pos += 1;
return Ok(Value::Object(pairs));
}
loop {
self.skip_ws();
if self.peek() != Some(b'"') {
return Err("expected string key in object".to_string());
}
let key = self.parse_string()?;
self.skip_ws();
if self.peek() != Some(b':') {
return Err("expected ':' after object key".to_string());
}
self.pos += 1;
let val = self.parse_value()?;
pairs.push((key, val));
self.skip_ws();
match self.peek() {
Some(b',') => {
self.pos += 1;
}
Some(b'}') => {
self.pos += 1;
break;
}
_ => return Err("expected ',' or '}' in object".to_string()),
}
}
Ok(Value::Object(pairs))
}
fn parse_array(&mut self) -> PResult<Value> {
self.pos += 1; // [
let mut items = Vec::new();
self.skip_ws();
if self.peek() == Some(b']') {
self.pos += 1;
return Ok(Value::Array(items));
}
loop {
let val = self.parse_value()?;
items.push(val);
self.skip_ws();
match self.peek() {
Some(b',') => {
self.pos += 1;
}
Some(b']') => {
self.pos += 1;
break;
}
_ => return Err("expected ',' or ']' in array".to_string()),
}
}
Ok(Value::Array(items))
}
fn parse_string(&mut self) -> PResult<String> {
self.pos += 1; // opening quote
let mut out = String::new();
while let Some(c) = self.peek() {
self.pos += 1;
match c {
b'"' => return Ok(out),
b'\\' => {
let e = self.peek().ok_or_else(|| "trailing escape".to_string())?;
self.pos += 1;
match e {
b'"' => out.push('"'),
b'\\' => out.push('\\'),
b'/' => out.push('/'),
b'n' => out.push('\n'),
b't' => out.push('\t'),
b'r' => out.push('\r'),
b'b' => out.push('\u{0008}'),
b'f' => out.push('\u{000C}'),
b'u' => {
let cp = self.parse_codepoint()?;
out.push(cp);
}
_ => return Err(format!("bad escape \\{}", e as char)),
}
}
_ => out.push(c as char),
}
}
Err("unterminated string".to_string())
}
fn parse_codepoint(&mut self) -> PResult<char> {
if self.pos + 4 > self.bytes.len() {
return Err("short \\u escape".to_string());
}
let hex = std::str::from_utf8(&self.bytes[self.pos..self.pos + 4])
.map_err(|_| "non-ascii in \\u escape".to_string())?;
self.pos += 4;
let mut code =
u32::from_str_radix(hex, 16).map_err(|_| "bad \\u escape".to_string())?;
// UTF-16 surrogate pair handling.
if (0xD800..=0xDBFF).contains(&code) {
if self.bytes[self.pos..].starts_with(b"\\u") {
self.pos += 2;
let lo_hex = std::str::from_utf8(&self.bytes[self.pos..self.pos + 4])
.map_err(|_| "non-ascii in low surrogate".to_string())?;
self.pos += 4;
let lo = u32::from_str_radix(lo_hex, 16)
.map_err(|_| "bad low surrogate".to_string())?;
if (0xDC00..=0xDFFF).contains(&lo) {
code = 0x10000 + ((code - 0xD800) << 10) + (lo - 0xDC00);
}
}
}
char::from_u32(code).ok_or_else(|| "invalid unicode codepoint".to_string())
}
fn parse_bool(&mut self) -> PResult<Value> {
if self.bytes[self.pos..].starts_with(b"true") {
self.pos += 4;
Ok(Value::Bool(true))
} else if self.bytes[self.pos..].starts_with(b"false") {
self.pos += 5;
Ok(Value::Bool(false))
} else {
Err("invalid literal".to_string())
}
}
fn parse_null(&mut self) -> PResult<Value> {
if self.bytes[self.pos..].starts_with(b"null") {
self.pos += 4;
Ok(Value::Null)
} else {
Err("invalid literal".to_string())
}
}
fn parse_number(&mut self) -> PResult<Value> {
let start = self.pos;
if self.peek() == Some(b'-') {
self.pos += 1;
}
while let Some(c) = self.peek() {
match c {
b'0'..=b'9' | b'.' | b'e' | b'E' | b'+' | b'-' => self.pos += 1,
_ => break,
}
}
let s = std::str::from_utf8(&self.bytes[start..self.pos])
.map_err(|_| "non-utf8 number".to_string())?;
let n: f64 = s.parse().map_err(|_| format!("bad number {}", s))?;
Ok(Value::Number(n))
}
}
/// Parse a JSON document into a [Value]. This is the entry point for callers
/// holding a raw JSON string.
pub fn parse_json(input: &str) -> PResult<Value> {
let mut p = Parser::new(input);
let v = p.parse_value()?;
p.skip_ws();
if p.pos != p.bytes.len() {
return Err(format!("trailing data at byte {}", p.pos));
}
Ok(v)
}
// ---------------------------------------------------------------------------
// Type tree
// ---------------------------------------------------------------------------
// A recursive enum describing an inferred TS type. `signature` (below) is a
// structural fingerprint used to dedupe identical object shapes independent of
// the names we eventually assign.
#[derive(Clone)]
enum TypeNode {
Primitive(String),
Unknown,
// `None` element type means `unknown[]`.
Array(Option<Box<TypeNode>>),
Union(Vec<TypeNode>),
Object {
props: Vec<PropNode>,
name_hint: String,
},
}
#[derive(Clone)]
struct PropNode {
key: String,
typ: TypeNode,
optional: bool,
}
fn null_node() -> TypeNode {
TypeNode::Primitive("null".to_string())
}
/// A structural fingerprint of a type, independent of assigned names. Object
/// signatures include each key (with optionality) and recurse, so two objects
/// with the same shape always share a signature.
fn signature(n: &TypeNode) -> String {
match n {
TypeNode::Primitive(s) => s.clone(),
TypeNode::Unknown => "?".to_string(),
TypeNode::Array(of) => match of {
Some(inner) => format!("[{}]", signature(inner)),
None => "[]".to_string(),
},
TypeNode::Union(members) => {
let mut parts: Vec<String> = members.iter().map(signature).collect();
let mut joined = parts.join("|");
joined.insert(0, '(');
joined.push(')');
joined
}
TypeNode::Object { props, .. } => {
let body: String = props
.iter()
.map(|p| {
let opt = if p.optional { "?" } else { "" };
format!("{}{}:{}", p.key, opt, signature(&p.typ))
})
.collect::<Vec<_>>()
.join(";");
format!("{{{}}}", body)
}
}
}
// ---------------------------------------------------------------------------
// 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` so the result is a valid TS identifier.
fn pascal(key: &str) -> String {
let segs: Vec<&str> = key.split(|c: char| !c.is_ascii_alphanumeric()).filter(|s| !s.is_empty()).collect();
let head = if segs.is_empty() {
"Item".to_string()
} else {
let mut b = String::new();
for s in &segs {
let mut chars = s.chars();
if let Some(first) = chars.next() {
for c in first.to_uppercase() {
b.push(c);
}
b.push_str(chars.as_str());
}
}
b
};
if head.starts_with(|c: char| c.is_ascii_digit()) {
format!("N{}", head)
} else {
head
}
}
/// Singularize an interface name for array-element naming (`Items` -> `Item`).
/// Trims a trailing `s` (never `ss`); otherwise appends `Item`.
fn singularize(name: &str) -> String {
let n = name.len();
if n > 1 && name.ends_with('s') && !name.ends_with("ss") {
name[..n - 1].to_string()
} else {
format!("{}Item", name)
}
}
/// Coerce a user-supplied root name into a valid TS identifier (PascalCase).
fn sanitize_root(name: &str) -> String {
let cleaned = pascal(name);
if cleaned.is_empty() {
"Root".to_string()
} else {
cleaned
}
}
// ---------------------------------------------------------------------------
// Input validation
// ---------------------------------------------------------------------------
// Because `Value` is a sealed enum of exactly the JSON kinds, every input is
// JSON-serializable by construction — the runtime check the TS lib performs is
// a compile-time guarantee here. The function is kept for API parity.
fn assert_json_serializable(_v: &Value, _path: &str) -> Result<(), String> {
Ok(())
}
// ---------------------------------------------------------------------------
// Shape helpers
// ---------------------------------------------------------------------------
/// Does a type contain a `null` leaf? Used by `optional_nullable`.
fn contains_null(n: &TypeNode) -> bool {
match n {
TypeNode::Primitive(s) => s == "null",
TypeNode::Union(members) => members.iter().any(contains_null),
_ => false,
}
}
/// Dedupe nodes by structural signature, preserving first-seen order.
fn dedupe(nodes: &[TypeNode]) -> Vec<TypeNode> {
let mut seen: HashSet<String> = HashSet::new();
let mut out: Vec<TypeNode> = Vec::new();
for n in nodes {
let s = signature(n);
if seen.insert(s) {
out.push(n.clone());
}
}
out
}
// ---------------------------------------------------------------------------
// Combining array element types
// ---------------------------------------------------------------------------
/// Merge several object nodes into one: union of keys (keys absent from some
/// element become optional), recursing per key.
fn merge_objects(objs: &[TypeNode], opts: &Options) -> TypeNode {
// We know every entry is an Object; extract (props, name_hint) pairs.
let mut key_order: Vec<String> = Vec::new();
let mut by_key: HashMap<String, Vec<TypeNode>> = HashMap::new();
let first_hint = if let TypeNode::Object { name_hint, .. } = &objs[0] {
name_hint.clone()
} else {
String::new()
};
for o in objs {
if let TypeNode::Object { props, .. } = o {
for p in props {
if !by_key.contains_key(&p.key) {
key_order.push(p.key.clone());
}
by_key.entry(p.key.clone()).or_default().push(p.typ.clone());
}
}
}
let total = objs.len();
let mut props: Vec<PropNode> = Vec::with_capacity(key_order.len());
for key in key_order {
let child_types = by_key.remove(&key).unwrap_or_default();
let typ = combine(&child_types, opts);
let mut optional = child_types.len() < total; // missing from some element
if opts.optional_nullable && contains_null(&typ) {
optional = true;
}
props.push(PropNode {
key: key.clone(),
typ,
optional,
});
}
TypeNode::Object {
props,
name_hint: first_hint,
}
}
/// Combine a list of element types into one. empty -> unknown; `union_arrays`
/// -> distinct union; otherwise merge where sensible (objects merge keys,
/// distinct primitives union) and build a union only for genuinely
/// heterogeneous input.
fn combine(nodes: &[TypeNode], opts: &Options) -> TypeNode {
if nodes.is_empty() {
return TypeNode::Unknown;
}
if opts.union_arrays {
let d = dedupe(nodes);
return if d.len() == 1 {
d.into_iter().next().unwrap()
} else {
TypeNode::Union(d)
};
}
let mut objs: Vec<&TypeNode> = Vec::new();
let mut arrs: Vec<&TypeNode> = Vec::new();
let mut prims: Vec<TypeNode> = Vec::new();
let mut has_unknown = false;
for n in nodes {
match n {
TypeNode::Object { .. } => objs.push(n),
TypeNode::Array(_) => arrs.push(n),
TypeNode::Primitive(_) => prims.push(n.clone()),
TypeNode::Unknown => has_unknown = true,
// A Union fed back into combine (non-union mode) matches none of
// the TS filters and is dropped — preserved here for parity.
TypeNode::Union(_) => {}
}
}
let prims = dedupe(&prims);
// Pure primitive/unknown arrays collapse: identical -> single, distinct -> union.
if objs.is_empty() && arrs.is_empty() {
let mut members = prims.clone();
if has_unknown {
members.push(TypeNode::Unknown);
}
let d = dedupe(&members);
return if d.len() == 1 {
d.into_iter().next().unwrap()
} else {
TypeNode::Union(d)
};
}
// Homogeneous object array -> a single merged object.
if !objs.is_empty()
&& arrs.is_empty()
&& prims.is_empty()
&& !has_unknown
{
let owned: Vec<TypeNode> = objs.iter().cloned().collect();
return merge_objects(&owned, opts);
}
// Otherwise build a union of the meaningful parts.
let mut members: Vec<TypeNode> = Vec::new();
if !objs.is_empty() {
let owned: Vec<TypeNode> = objs.iter().cloned().collect();
members.push(merge_objects(&owned, opts));
}
if !arrs.is_empty() {
let mut of_types: Vec<TypeNode> = Vec::new();
for a in &arrs {
if let TypeNode::Array(Some(of)) = a {
of_types.push((**of).clone());
} else {
of_types.push(TypeNode::Unknown);
}
}
let of = if of_types.is_empty() {
None
} else {
Some(Box::new(combine(&of_types, opts)))
};
members.push(TypeNode::Array(of));
}
members.extend(prims.iter().cloned());
if has_unknown {
members.push(TypeNode::Unknown);
}
let d = dedupe(&members);
if d.len() == 1 {
d.into_iter().next().unwrap()
} else {
TypeNode::Union(d)
}
}
// ---------------------------------------------------------------------------
// Inference
// ---------------------------------------------------------------------------
/// Recursively infer a type tree from a JSON value. `hint` is the interface
/// name to use if this value is an object.
fn infer(value: &Value, hint: &str, opts: &Options) -> TypeNode {
match value {
Value::Null => null_node(),
Value::Bool(_) => TypeNode::Primitive("boolean".to_string()),
Value::Number(_) => TypeNode::Primitive("number".to_string()),
Value::Str(_) => TypeNode::Primitive("string".to_string()),
Value::Array(items) => {
if items.is_empty() {
return TypeNode::Array(None);
}
let elem_hint = singularize(hint);
let elements: Vec<TypeNode> =
items.iter().map(|e| infer(e, &elem_hint, opts)).collect();
TypeNode::Array(Some(Box::new(combine(&elements, opts))))
}
Value::Object(pairs) => {
// plain object (serializability guaranteed by the Value type)
let mut props: Vec<PropNode> = Vec::with_capacity(pairs.len());
for (key, v) in pairs {
let child_hint = format!("{}{}", hint, pascal(key));
let typ = infer(v, &child_hint, opts);
let optional = opts.optional_nullable && contains_null(&typ);
props.push(PropNode {
key: key.clone(),
typ,
optional,
});
}
TypeNode::Object {
props,
name_hint: hint.to_string(),
}
}
}
}
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
/// TypeScript reserved words + built-in type names. Any object key appearing
/// here (or not a bareword identifier) must be emitted as a quoted string key.
const RESERVED: &[&str] = &[
"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",
];
fn is_bare_ident(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c == '_' || c == '$' || c.is_ascii_alphabetic() => {}
_ => return false,
}
chars.all(|c| c == '_' || c == '$' || c.is_ascii_alphanumeric())
}
/// Quote-and-escape a key as a JSON string literal.
fn quote_key(key: &str) -> String {
let mut out = String::with_capacity(key.len() + 2);
out.push('"');
for c in key.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
/// Emit a bareword key when legal, else a quoted JSON string literal.
fn render_key(key: &str) -> String {
if is_bare_ident(key) && !RESERVED.contains(&key) {
key.to_string()
} else {
quote_key(key)
}
}
/// Wrap an array element in parens if it would otherwise mis-parse (a union).
fn wrap_array(rendered: String, of: &TypeNode) -> String {
match of {
TypeNode::Union(_) => format!("({})", rendered),
_ => rendered,
}
}
fn render_type(n: &TypeNode, names: &HashMap<String, String>) -> String {
match n {
TypeNode::Primitive(s) => s.clone(),
TypeNode::Unknown => "unknown".to_string(),
TypeNode::Object { .. } => names
.get(&signature(n))
.cloned()
.unwrap_or_else(|| "unknown".to_string()),
TypeNode::Array(of) => match of {
None => "unknown[]".to_string(),
Some(inner) => format!("{}[]", wrap_array(render_type(inner, names), inner)),
},
TypeNode::Union(members) => dedupe(members)
.iter()
.map(|m| render_type(m, names))
.collect::<Vec<_>>()
.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
/// `root_name`; an array-of-objects root names its element `<Root>Item`.
fn collect_objects(root: &TypeNode, root_name: &str) -> (Vec<TypeNode>, HashMap<String, String>) {
let mut names: HashMap<String, String> = HashMap::new();
let mut used: HashSet<String> = HashSet::new();
let mut order: Vec<TypeNode> = Vec::new();
fn push_prop_types(props: &[PropNode]) -> Vec<TypeNode> {
props.iter().map(|p| p.typ.clone()).collect()
}
fn visit(
n: &TypeNode,
is_root: bool,
root_name: &str,
names: &mut HashMap<String, String>,
used: &mut HashSet<String>,
order: &mut Vec<TypeNode>,
) {
if let TypeNode::Object { props, name_hint } = n {
let sig = signature(n);
if !names.contains_key(&sig) {
let mut candidate = if is_root {
root_name.to_string()
} else {
name_hint.clone()
};
if used.contains(&candidate) {
let mut i = 2;
while used.contains(&format!("{}{}", candidate, i)) {
i += 1;
}
candidate = format!("{}{}", candidate, i);
}
names.insert(sig.clone(), candidate.clone());
used.insert(candidate);
order.push(n.clone());
for t in push_prop_types(props) {
visit(&t, false, root_name, names, used, order);
}
} else {
// already named — still recurse to discover new nested shapes
for t in push_prop_types(props) {
visit(&t, false, root_name, names, used, order);
}
}
} else if let TypeNode::Array(Some(of)) = n {
visit(of, false, root_name, names, used, order);
} else if let TypeNode::Union(members) = n {
for m in members {
visit(m, false, root_name, names, used, order);
}
}
}
visit(root, true, root_name, &mut names, &mut used, &mut order);
(order, names)
}
fn render_interface(n: &TypeNode, names: &HashMap<String, String>) -> String {
if let TypeNode::Object { props, .. } = n {
let name = names.get(&signature(n)).cloned().unwrap_or_default();
if props.is_empty() {
return format!("interface {} {{}}", name);
}
let lines: Vec<String> = props
.iter()
.map(|p| {
let opt = if p.optional { "?" } else { "" };
format!(
" {}{}: {};",
render_key(&p.key),
opt,
render_type(&p.typ, names)
)
})
.collect();
format!("interface {} {{\n{}\n}}", name, lines.join("\n"))
} else {
String::new()
}
}
// ---------------------------------------------------------------------------
// Public entry point
// ---------------------------------------------------------------------------
/// Infer TypeScript interfaces from any JSON-serializable value.
///
/// Returns the emitted source directly: because `Value` guarantees
/// JSON-serializability at compile time, inference cannot fail at runtime.
pub fn json_to_ts(value: &Value, opts: &Options) -> String {
let _ = assert_json_serializable(value, ""); // parity no-op; see note above
let resolved = resolve(opts);
let root = infer(value, &resolved.root_name, &resolved);
// Primitive / unknown / array roots emit a `type` alias; object roots emit interfaces.
match &root {
TypeNode::Object { .. } => {
let (order, names) = collect_objects(&root, &resolved.root_name);
order
.iter()
.map(|o| render_interface(o, &names))
.collect::<Vec<_>>()
.join("\n\n")
}
TypeNode::Array(_) => {
let (order, names) = collect_objects(&root, &resolved.root_name);
let ifaces = order
.iter()
.map(|o| render_interface(o, &names))
.collect::<Vec<_>>()
.join("\n\n");
let alias = format!(
"type {} = {};",
resolved.root_name,
render_type(&root, &names)
);
if ifaces.is_empty() {
alias
} else {
format!("{}\n\n{}", ifaces, alias)
}
}
// primitive or unknown root
_ => format!(
"type {} = {};",
resolved.root_name,
render_type(&root, &HashMap::new())
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_object() {
let v = parse_json(r#"{"name":"a","age":1}"#).unwrap();
let out = json_to_ts(&v, &Options::default());
assert_eq!(out, "interface Root {\n name: string;\n age: number;\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 →