typescriptabstract-syntax-treetypescript-compiler-apits-morph

How to use ts compiler api or ts-morph to get and serialize type information


I am trying to use the Typescript API to get information from typescript file that imports types from other sources.

I got common type:

// example.types.ts

export interface Example<T> {
  description: string;
  title: string;
  element: T;
}

Then some element component or class I want to prepare example for

// Canvas.ts

interface ICanvasConfig {
  name: string;
  size: {
    width: number;
    height: number;
  };
}

export const Canvas = (config: ICanvasConfig): void => {
  console.log(config);
};

And this is the target file I want to parse

// Canvas.example.ts

import type { Example } from './example.types';

import { Canvas } from './Canvas';

const exampleMeta: Example<typeof Canvas> = {
  title: 'Canvas Element',
  description: 'Element used for..',
  element: Canvas
};

export default exampleMeta;

What I'm expecting is to get at the end something like

{
  title: {
     value: 'Canvas Element',
     type: 'string'
  }
  description: {
    value: 'Element used for..',
    type: 'string',
  },
  element: {
    value: Canvas, // real value
    type: {
      name: 'function',
      args: [{
          name: 'string',
          size: {
            width: 'number',
            height: 'number'
          }
      }],
      ret: 'void'
    }
  }
}

I tried to use ts compiler and ts-morph but all I do is beating around the bush. I wont publish my solution attempts not to distract you, but it seems I don't understand the inner structure of ts nodes to get what I need. Maximum what I got playing around is detecting title, description as a string but any type for element.

So my question is it actually possible using these tools? If yes then how? Are there any better solutions to achieve what I need?

Or if you ever faced with similar problems sharing your experience would be much appreciated.


Solution

  • You can achieve that using ts-morph with the code below. This code only works for the example code you provided, but it should be easy to implement a more generic solution using recursion.

    I also found a related GitHub issue "Question: Is there a simple way to get a type reduced to only primitives?" and it has a library for expanding types https://github.com/dsherret/ts-morph/issues/1204#issuecomment-961546054, although it does not extract values such as 'Canvas Element'. To use that add type exampleMeta = typeof exampleMeta to Canvas.example.ts and run import { typeFootprint } from "./typeFootprint"; console.log(typeFootprint("src/Canvas.example.ts", "exampleMeta")); to get the string *1.

    import { Project, SyntaxKind } from "ts-morph";
    import util from "util"  // for displaying a deep object
    
    const project = new Project({})
    
    // Assuming the files `example.types.ts`, `Canvas.ts`, and `Canvas.example.ts` are in `./src`.
    project.addSourceFilesAtPaths("src/**/*.ts")
    const exampleMeta = project.getSourceFile("src/Canvas.example.ts")!.getVariableDeclaration("exampleMeta")!
    
    const exampleMetaValue = exampleMeta
        // "go to definition"
        .getFirstChildByKindOrThrow(SyntaxKind.Identifier)
        .getDefinitionNodes()[0]!
    
        // get the initializer `{ title: ..., description: ..., ... }`
        .asKindOrThrow(SyntaxKind.VariableDeclaration)
        .getInitializerOrThrow()
    
    // "go to type definition"
    const exampleMetaType = exampleMeta
        .getType()
    
    console.log(util.inspect({
        // You can list properties with `exampleMetaType.getProperties().map((property) => property.getName())`
        title: {
            value: exampleMetaValue
                // .title
                .asKindOrThrow(SyntaxKind.ObjectLiteralExpression)  // You can check the kind with `.isKind()` or `.getKindName()`
                .getPropertyOrThrow("title")
    
                // get the initializer `Create Element`
                .asKindOrThrow(SyntaxKind.PropertyAssignment)
                .getInitializerOrThrow()
                .getText(),
            type: exampleMetaType
                // .title
                .getPropertyOrThrow("title")
                .getTypeAtLocation(exampleMeta)
                .getText(),
        },
        description: {
            value: exampleMetaValue
                // .description
                .asKindOrThrow(SyntaxKind.ObjectLiteralExpression)
                .getPropertyOrThrow("description")
    
                // get the initializer `Element used for..`
                .asKindOrThrow(SyntaxKind.PropertyAssignment)
                .getInitializerOrThrow()
                .getText(),
            type: exampleMetaType
                // .description
                .getPropertyOrThrow("description")
                .getTypeAtLocation(exampleMeta)
                .getText(),
        },
        element: {
            value: exampleMetaValue
                // .element
                .asKindOrThrow(SyntaxKind.ObjectLiteralExpression)
                .getPropertyOrThrow("element")
    
                // get the initializer `Canvas`
                .asKindOrThrow(SyntaxKind.PropertyAssignment)
                .getInitializerOrThrow()
    
                .getText(),
            type: {
                name: exampleMetaType
                    // .element
                    .getPropertyOrThrow("element")
                    .getTypeAtLocation(exampleMeta)
                    .getCallSignatures().length > 0 ? "function" : "",
                args: exampleMetaType
                    // .element
                    .getPropertyOrThrow("element")
                    .getTypeAtLocation(exampleMeta)
                    // Parse '(config: ICanvasConfig) => void'
                    .getCallSignatures()[0]!
                    .getParameters().map((arg) => {
                        const iCanvasPropertyType: any = {}
                        for (const iCanvasConfigProperty of arg.getTypeAtLocation(exampleMeta).getProperties()) {
                            iCanvasPropertyType[iCanvasConfigProperty.getName()] = iCanvasConfigProperty
                                .getTypeAtLocation(exampleMeta)
                                .getText()  // TODO: Parse '{ width: number; height: number; }'
                        }
                        return iCanvasPropertyType
                    }),
                ret: exampleMetaType
                    // .element
                    .getPropertyOrThrow("element")
                    .getTypeAtLocation(exampleMeta)
    
                    // Parse '(config: ICanvasConfig) => void'
                    .getCallSignatures()[0]!
                    .getReturnType()
                    .getText(),   // .getFlags() & TypeFlags.Void === true
            },
        },
    }, { depth: null }))
    

    *1

    type exampleMeta = {
      description: string;
      title: string;
      element(config: {
        name: string;
        size: {
          width: number;
          height: number;
        };
      }): void;
    }