typescriptts-morph

How create TypeLiteral without string/writer when using ts-morph?


I want to create type files like this programatically:

export type Car = {
    color: string;
    // ... tons of properties
};

Thats seems pretty easy using ts-morph addTypeAlias method:

sourceFile.addTypeAlias({
    name: 'Car',
    type: theType,
    isExported: true,
});

Though unfortunately theType here must be string | WriterFunction. And I want it to be a TypeLiteral! Is there a way to this with ts-morph that I am missing? Ofcourse I could construct my type literal as a string by some looping and string concatenation etc, but I would really like to avoid building my files that way - then I might as well use template/stub files and fs.

Any other way to do this in a more programmatic/Node-class based approach?


Solution

  • Introduction

    Let's consider the following Node.js version as the current version:

    $ node --version
    v18.13.0
    

    Let's consider the following npm version as the current version:

    $ npm --version
    9.2.0
    

    Let's consider the following versions of the dependencies as the current versions (an excerpt from the package.json file):

    {
      <…>,
      "dependencies": {
        "ts-morph": "17.0.1",
        "typescript": "4.9.5"
      },
      "devDependencies": {
        "ts-node": "10.9.1"
      }
    }
    

    Overview

    Though unfortunately theType here must be string | WriterFunction. And I want it to be a TypeLiteral! Is there a way to this with ts-morph that I am missing?

    Currently, it seems there is no straightforward solution.

    Please, see the following seemingly related GitHub issues:

    Any other way to do this in a more programmatic/Node-class based approach?

    Let's consider some possible solutions.

    Possible solution: ts-morph: Use object type writer (Writers.objectType() static method)

    It may be considered as more programmatic, but it is still a writer-based (not a node-based) approach.

    References:

    Draft example

    src/index-ts-morph.ts file

    import { Project, WriterFunction, Writers } from "ts-morph";
    
    async function createFile(outputFilePath: string) {
        const project = new Project();
        const sourceFile = project.createSourceFile(
            outputFilePath,
            undefined,
            { overwrite: true }
        );
    
        const typeAliasDeclarations = [
            createExportedTypeAliasDeclaration("Book"),
            createExportedTypeAliasDeclaration("House"),
            createExportedTypeAliasDeclaration("Car"),
        ];
        sourceFile.addTypeAliases(typeAliasDeclarations);
    
        await sourceFile.save();
    };
    
    function createExportedTypeAliasDeclaration(name: string) {
        const writerFunction: WriterFunction = Writers.objectType({
            properties: [
                {
                    name: "color",
                    type: "string"
                }
            ]
        });
        return ({
            name: name,
            type: writerFunction,
            isExported: true,
        });
    }
    
    await createFile("output.ts");
    

    Running script and checking its output

    $ npx ts-node src/index-ts-morph.ts
    $ cat output.ts 
    export type Book = {
            color: string;
        };
    export type House = {
            color: string;
        };
    export type Car = {
            color: string;
        };
    

    Please, note the strange indentation.
    Probably, it is caused by a ts-morph defect?

    Possible solution: typescript: Use typescript instead of ts-morph

    References:

    Draft example

    src/index-typescript.ts file

    import { writeFile } from "fs/promises";
    import ts from "typescript";
    const { factory } = ts;
    
    async function createFile(outputFilePath: string) {
        const typeAliasDeclarations = [
            createExportedTypeAliasDeclaration("Book"),
            createExportedTypeAliasDeclaration("House"),
            createExportedTypeAliasDeclaration("Car"),
        ];
    
        const printer = ts.createPrinter({
            newLine: ts.NewLineKind.LineFeed
        });
        const nodes = factory.createNodeArray(typeAliasDeclarations);
        const sourceFile = ts.createSourceFile(
            "this-name-is-ignored.ts",
            "",
            ts.ScriptTarget.Latest,
            false,
            ts.ScriptKind.TS
        );
        const printedString = printer.printList(
            ts.ListFormat.MultiLine,
            nodes,
            sourceFile
        );
    
        await writeFile(outputFilePath, printedString);
    };
    
    function createExportedTypeAliasDeclaration(name: string) {
        const colorPropertySignature = factory.createPropertySignature(
            undefined,
            factory.createIdentifier("color"),
            undefined,
            factory.createTypeReferenceNode(
                factory.createIdentifier("string"),
                undefined
            )
        );
    
        return factory.createTypeAliasDeclaration(
            [
                factory.createToken(ts.SyntaxKind.ExportKeyword)
            ],
            factory.createIdentifier(name),
            undefined,
            factory.createTypeLiteralNode([
                colorPropertySignature
            ])
        );
    }
    
    await createFile("output.ts");
    

    Running script and checking its output

    $ npx ts-node src/index-typescript.ts
    $ cat output.ts 
    export type Book = {
        color: string;
    };
    export type House = {
        color: string;
    };
    export type Car = {
        color: string;
    };