How to convert a json to a typescript interface?

Solution 1:

You can write the script using typescript compiler API and its ability to infer types. I was really surprised how easy it was.

You have to wrap your sample data to make it compile-able as typescript code. The script will pick all variable declarations and try to print inferred types for them. It uses variable names and property names for assigning names to types, and if two objects have a property with the same name, it will pick the type from the first one. So it will not work in case these types are actually different (the fix is left as an exercise). For your JSON output, the data sample will look like

file sample.ts

let raceCarDriver = {
    "id": 13,
    "name": "horst",
    "cars": [{
        "brand": "VW",
        "maxSpeed": 120,
        "isWastingGazoline": true,
    }]
};

The script was tested with Typescript 2.1 (just released):

 npm i typescript
 npm i @types/node
 ./node_modules/.bin/tsc --lib es6 print-inferred-types.ts
 node print-inferred-types.js sample.ts

output:

export interface RaceCarDriver {
    id: number;
    name: string;
    cars: Car[];
}
export interface Car {
    brand: string;
    maxSpeed: number;
    isWastingGazoline: boolean;
}

Here is the script: print-inferred-types.ts:

import * as ts from "typescript";

let fileName = process.argv[2];

function printInferredTypes(fileNames: string[], options: ts.CompilerOptions): void {
    let program = ts.createProgram(fileNames, options);
    let checker = program.getTypeChecker();

    let knownTypes: {[name: string]: boolean} = {};
    let pendingTypes: {name: string, symbol: ts.Symbol}[] = [];

    for (const sourceFile of program.getSourceFiles()) {
        if (sourceFile.fileName == fileName) {
            ts.forEachChild(sourceFile, visit);
        }
    }

    while (pendingTypes.length > 0) {
        let pendingType = pendingTypes.shift();
        printJsonType(pendingType.name, pendingType.symbol);
    }


    function visit(node: ts.Node) {
        if (node.kind == ts.SyntaxKind.VariableStatement) {
            (<ts.VariableStatement>node).declarationList.declarations.forEach(declaration => {
                if (declaration.name.kind == ts.SyntaxKind.Identifier) {
                    let identifier = <ts.Identifier>declaration.name;
                    let symbol = checker.getSymbolAtLocation(identifier);
                    if (symbol) {
                        let t = checker.getTypeOfSymbolAtLocation(symbol, identifier);
                        if (t && t.symbol) {
                            pendingTypes.push({name: identifier.text, symbol: t.symbol});
                        }
                    }
                }
            });
        }
    }

    function printJsonType(name: string, symbol: ts.Symbol) {
        if (symbol.members) {
            console.log(`export interface ${capitalize(name)} {`);
            Object.keys(symbol.members).forEach(k => {
                let member = symbol.members[k];
                let typeName = null;
                if (member.declarations[0]) {
                    let memberType = checker.getTypeOfSymbolAtLocation(member, member.declarations[0]);
                    if (memberType) {
                        typeName = getMemberTypeName(k, memberType);
                    }
                }
                if (!typeName) {
                    console.log(`// Sorry, could not get type name for ${k}!`);
                } else {
                    console.log(`    ${k}: ${typeName};`);
                }
            });
            console.log(`}`);
        }
    }

    function getMemberTypeName(memberName: string, memberType: ts.Type): string | null {
        if (memberType.flags == ts.TypeFlags.String) {
            return 'string';
        } else if (memberType.flags == ts.TypeFlags.Number) {
            return 'number';
        } else if (0 !== (memberType.flags & ts.TypeFlags.Boolean)) {
            return 'boolean';
        } else if (memberType.symbol) {
            if (memberType.symbol.name == 'Array' && (<ts.TypeReference>memberType).typeArguments) {
                let elementType = (<ts.TypeReference>memberType).typeArguments[0];
                if (elementType && elementType.symbol) {
                    let elementTypeName = capitalize(stripS(memberName));
                    if (!knownTypes[elementTypeName]) {
                        knownTypes[elementTypeName] = true;
                        pendingTypes.push({name: elementTypeName, symbol: elementType.symbol});
                    }
                    return `${elementTypeName}[]`;
                }
            } else if (memberType.symbol.name == '__object') {
                let typeName = capitalize(memberName);
                if (!knownTypes[typeName]) {
                    knownTypes[typeName] = true;
                    pendingTypes.push({name: typeName, symbol: memberType.symbol});
                }
                return typeName;
            } else {
                return null;
            }
        } else {
            return null;
        }
    }

    function capitalize(n: string) {
        return n.charAt(0).toUpperCase() + n.slice(1);
    }
    function stripS(n: string) {
        return n.endsWith('s') ? n.substring(0, n.length - 1) : n;
    }
}

printInferredTypes([fileName], {
    noEmitOnError: true, noImplicitAny: true,
    target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS
});

Solution 2:

Found a npm package that converts a arbitrary JSON file without a schema into a TS interface: https://www.npmjs.com/package/json-to-ts

The author also provided a VSCode plugin.

Solution 3:

You can use an npm module instead of the web hosted solution:

https://www.npmjs.com/package/json-schema-to-typescript

If your JSON comes from an HTTP API and the API has an swagger code definition, you can generate a TypeScript client:

https://github.com/swagger-api/swagger-codegen#api-clients

If you json comes from a Java ot .Net backened, you can generate TypeScript from Java or C# classes:

http://type.litesolutions.net

https://github.com/raphaeljolivet/java2typescript

Solution 4:

Using only sed and tsc

sed '1s@^@const foo = @' sample.json > sample.$$.ts
tsc sample.$$.ts --emitDeclarationOnly --declaration
  1. Append const foo = to beginning of file
    Using sed to replace (s) nothing (@^@) at the beginning of the first line (1) with const foo =
  2. output to sample.$$.ts
    the extension is the required to be .ts
    $$ expands to the shells process id, handy for a temp file that is unlikely to overwrite stuff you care about
  3. ask tsc to only emit a .d.ts typings file
    this file has pretty much everything you want for the interface. You might need to replace a few strings and customize it in a way you want but most of the leg work is done