Skip to content

[Feat] JSON → TypeScript Interface Generator #118

Description

@FumingPower3925

Description

Paste any JSON (API response, config, database record) and get TypeScript interface or type definitions with smart type inference. Handles nested objects, mixed arrays, optional fields, nullable types, and common patterns like date strings and enums. Every frontend developer copies JSON from an API and needs types — this automates it.

Features

  • Smart type inference:
    • Primitives: string, number, boolean, null
    • Date detection: ISO 8601 strings → string with /** @format date-time */ JSDoc comment
    • URL detection: string with /** @format uri */ annotation
    • UUID detection: string with /** @format uuid */ annotation
    • Enum detection: when an array of objects has a field with few unique string values → string literal union
    • Tuple detection: fixed-length arrays with different types → [string, number, boolean]
  • Array handling:
    • Uniform arrays: string[], User[]
    • Mixed arrays: (string | number)[]
    • Array of objects: merge all objects' keys into one interface, mark keys not present in all objects as optional
  • Nested objects → separate named interfaces with references
  • Optional fields — When generating from an array of objects, fields missing in some objects become fieldName?: type
  • Nullablenull values → fieldName: string | null
  • Output options:
    • interface vs type alias
    • readonly modifier on all properties
    • Optional semicolons vs no semicolons
    • Root type name (default: Root)
    • Nested interface naming: PascalCase from key name
    • Export keyword toggle
    • Indent: 2 spaces / 4 spaces / tabs
    • Sort keys alphabetically (optional)
  • Multiple roots — Paste a JSON array of objects → generates interface from merged shape
  • Inline vs extracted — Nested objects as inline { ... } or extracted to separate named interfaces
  • Copy / Download — Copy as .ts content or download as .d.ts file

Implementation

Dependencies

ZeroJSON.parse() + recursive type analysis.

Core Logic

interface GeneratorOptions {
    rootName: string;
    style: 'interface' | 'type';
    readonly: boolean;
    semicolons: boolean;
    exportKeyword: boolean;
    indent: string;
    sortKeys: boolean;
    inlineNested: boolean;
}

interface TypeInfo {
    kind: 'primitive' | 'array' | 'object' | 'union' | 'tuple' | 'literal';
    value: string;                    // For primitives: 'string', 'number', etc.
    properties?: Map<string, { type: TypeInfo; optional: boolean }>; // For objects
    elementType?: TypeInfo;           // For arrays
    members?: TypeInfo[];             // For unions/tuples
    interfaceName?: string;           // For extracted nested interfaces
}

function inferType(value: unknown, keyHint?: string): TypeInfo {
    if (value === null) return { kind: 'primitive', value: 'null' };
    if (typeof value === 'string') return { kind: 'primitive', value: detectStringSubtype(value) };
    if (typeof value === 'number') return { kind: 'primitive', value: Number.isInteger(value) ? 'number' : 'number' };
    if (typeof value === 'boolean') return { kind: 'primitive', value: 'boolean' };
    
    if (Array.isArray(value)) {
        if (value.length === 0) return { kind: 'array', value: 'unknown[]', elementType: { kind: 'primitive', value: 'unknown' } };
        
        const itemTypes = value.map(v => inferType(v));
        
        // All same type → T[]
        if (allSameType(itemTypes)) return { kind: 'array', value: '', elementType: itemTypes[0] };
        
        // Array of objects → merge into one interface with optional fields
        if (itemTypes.every(t => t.kind === 'object')) return { kind: 'array', value: '', elementType: mergeObjectTypes(itemTypes) };
        
        // Mixed primitives → union array
        const unique = deduplicateTypes(itemTypes);
        if (unique.length <= 3) return { kind: 'array', value: '', elementType: { kind: 'union', value: '', members: unique } };
        
        return { kind: 'array', value: 'unknown[]', elementType: { kind: 'primitive', value: 'unknown' } };
    }
    
    if (typeof value === 'object') {
        const props = new Map<string, { type: TypeInfo; optional: boolean }>();
        for (const [k, v] of Object.entries(value!)) {
            props.set(k, { type: inferType(v, k), optional: false });
        }
        return { kind: 'object', value: '', properties: props };
    }
    
    return { kind: 'primitive', value: 'unknown' };
}

function mergeObjectTypes(types: TypeInfo[]): TypeInfo {
    const allKeys = new Set(types.flatMap(t => [...(t.properties?.keys() ?? [])]));
    const merged = new Map<string, { type: TypeInfo; optional: boolean }>();
    
    for (const key of allKeys) {
        const values = types.map(t => t.properties?.get(key)).filter(Boolean);
        const optional = values.length < types.length; // Not in all objects
        const mergedType = values.length === 1 ? values[0]!.type : mergeTypes(values.map(v => v!.type));
        merged.set(key, { type: mergedType, optional });
    }
    
    return { kind: 'object', value: '', properties: merged };
}

function generateTypeScript(type: TypeInfo, options: GeneratorOptions): string {
    const interfaces: string[] = [];
    const rootType = typeToString(type, options.rootName, interfaces, options);
    
    const keyword = options.style === 'interface' ? 'interface' : 'type';
    const exp = options.exportKeyword ? 'export ' : '';
    const eq = options.style === 'type' ? ' = ' : ' ';
    
    // Root
    if (type.kind === 'object') {
        interfaces.push(`${exp}${keyword} ${options.rootName}${eq}${rootType}`);
    } else {
        interfaces.push(`${exp}type ${options.rootName} = ${rootType};`);
    }
    
    return interfaces.join('\n\n');
}

function detectStringSubtype(value: string): string {
    if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value)) return 'string'; // Date, but still string type
    return 'string';
}

Privacy

Pure client-side JSON parsing. API responses (which may contain sensitive data) never leave the browser.

UI

  • Left panel: JSON input textarea with syntax highlighting
  • Right panel: Generated TypeScript output with syntax highlighting and copy button
  • Options bar (between panels):
    • Root name input
    • interface / type toggle
    • readonly checkbox
    • export checkbox
    • Sort keys checkbox
    • Inline/extract nested toggle
    • Indent selector
  • Stats: Interface count, property count, detected patterns (dates, UUIDs, enums)
  • Download: .d.ts file
  • Responsive — panels stack on mobile

Files to Create/Modify

File Purpose
tools/json-to-ts/package.json Workspace package
tools/json-to-ts/src/tool.ts Type inference, merging, TypeScript generation
tools/json-to-ts/src/page.ts Split-panel UI, options bar
tools/json-to-ts/src/meta.ts ToolMeta
tools/json-to-ts/src/index.ts Exports
tools/json-to-ts/tests/tool.unit.test.ts Inference accuracy, merging, optional fields, edge cases
tools/json-to-ts/tests/page.unit.test.ts DOM rendering tests
src/index.ts Register route
src/index.html Add to tool grid

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions