13

Given a JSON output of an api:

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

I would like to define interfaces for typescript:

export interface Car {
    brand: string;
    maxSpeed: number;
    isWastingGazoline: boolean;
}

export interface RaceCarDriver {
    id: number;
    name: string;
    cars: Car[];
}

Yet I don't want them to type them manually, I rather have a script generate them for me.

How can I convert json into typescript interfaces? I also don't want to use webservices like MakeTypes or json2ts.

6
  • So how did you end up doing it? am looking for a solution too Commented Mar 7, 2018 at 19:47
  • 2
    @Jaya Most times I actually do use a webservice ^^' Like this: jvilk.com/MakeTypes or this: json2ts.com Commented Mar 8, 2018 at 15:04
  • thank you @k0pernikus for the quick reply! Commented Mar 8, 2018 at 20:16
  • github.com/quicktype/quicktype if using VS Code Commented Sep 17, 2022 at 20:54
  • 1
    Just built a simple tool that just does that. Hope it makes your life easier!😀 tson.vercel.app Commented Aug 18 at 14:23

4 Answers 4

15

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
});
Sign up to request clarification or add additional context in comments.

1 Comment

tnx .. just show name of interface properties are empty
14

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.

2 Comments

Pretty neat. I think this should be the accepted answer. It even has an online interface: jsontots.com.
VSCode plugin saved my life. Definitely should be the accepted answer.
4

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

2 Comments

json-schema-to-typescript changes a JSON Schema file (json-schema.org) to a TypeScript interface. This question is about inferring a TypeScript interface based on an arbitrary JSON file.
@JanAagaard in case you still looking for a module, see my answer below
1

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

3 Comments

It was not clear to me that you want to transform sample.json into sample.json.ts and that the expected result is a sample.json.ts. I also think your sed command does not create the sample.json.ts file but replaces the sample.json's content. I like the approach though, have an upvote.
NB that tsc expexts a file with .ts, .tsx or .d.ts extension. Ie. you cant do tsc /dev/stdin ... One might possibly be able to use a named pipe with fake extension ¯_(ツ)_/¯
@k0pernikus you're right, it creates a backup with .ts extension. I'll fix that

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.