How to convert a json to a typescript interface?
Asked Answered
C

4

12

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.

Contentment answered 10/12, 2016 at 0:1 Comment(5)
So how did you end up doing it? am looking for a solution tooOutgrow
@Outgrow Most times I actually do use a webservice ^^' Like this: jvilk.com/MakeTypes or this: json2ts.comContentment
thank you @Contentment for the quick reply!Outgrow
github.com/quicktype/quicktype if using VS CodeStylistic
If you're looking for a web tool to convert JSON to TypeScript interfaces, json-5.com/json-to-typescript is a popular choice. It's good for simple and frequent conversions, especially if type safety is a priority.Pickle
B
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
});
Bini answered 10/12, 2016 at 2:39 Comment(1)
tnx .. just show name of interface properties are emptyCummerbund
J
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.

Jumble answered 9/6, 2020 at 22:52 Comment(2)
Pretty neat. I think this should be the accepted answer. It even has an online interface: jsontots.com.Executrix
VSCode plugin saved my life. Definitely should be the accepted answer.Whitsunday
L
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

Lack answered 10/12, 2016 at 1:54 Comment(2)
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.Executrix
@JanAagaard in case you still looking for a module, see my answer belowJumble
P
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
Proofread answered 10/1, 2022 at 9:52 Comment(3)
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.Contentment
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 ¯_(ツ)_/¯Proofread
@Contentment you're right, it creates a backup with .ts extension. I'll fix thatProofread

© 2022 - 2024 — McMap. All rights reserved.