dsherret / ts-morph

TypeScript Compiler API wrapper for static analysis and programmatic code changes.
https://ts-morph.com
MIT License
5.03k stars 196 forks source link

How to extract ts type from record in ts-morph #1578

Open programandoconro opened 1 month ago

programandoconro commented 1 month ago

I am trying to create a type tree from extracted types in a typescript file. I am using bun and ts-morph I can handle different types, like primitives, and some objects. But when trying to handle Records, I cannot get the types that compose the record. To give an example, this is the test I want the code to pass:

import { expect, test } from "bun:test";
import createTypeTree from "../src/create-type-tree";

test.only("handle complex Records", () => {
  const sourceCode = `
      type Id = string;
      type Value = {a: number, b: string}
      type MyRecord = Record<string, number>;
`;
  const result = createTypeTree("temp.ts", sourceCode);
  expect(result).toStrictEqual({
    MyRecord: { ["string"]: { a: "number", b: "string" } },
    Id: "string",
    Value: { a: "number", b: "string" },
  });
});

This is a simplified and reproducible version of my code

import { Project, Type } from "ts-morph";

type Result = Record<string, unknown>;
export default function createTypeTree(
  filePath: string,
  testCode?: string,
  configFile?: string
): Result {
  const project = new Project({
    tsConfigFilePath: configFile,
    skipAddingFilesFromTsConfig: true,
  });
  const sourceFile = !testCode
    ? project.addSourceFileAtPath(filePath)
    : project.createSourceFile(filePath, testCode);

  const result = [
    ...sourceFile?.getInterfaces(),
    ...sourceFile?.getTypeAliases(),
  ].reduce((acc, current) => {
    const name = current.getName();
    acc[name] = handleTypes(current.getType());
    return acc;
  }, {} as Result);

  return result;
}
function handleTypes(t?: Type) {
  return isPrimitive(t) ? handlePrimitive(t) : handleNotPrimitive(t);
}

function isPrimitive(t?: Type): boolean {
  const isString = t?.isString() || t?.isStringLiteral();
  const isBoolean = t?.isBoolean() || t?.isBooleanLiteral();
  const isNumber = t?.isNumber() || t?.isNumberLiteral();
  const isNullish = t?.isNullable();

  return Boolean(isNumber || isString || isBoolean || isNullish);
}

function handlePrimitive(t?: Type) {
  switch (true) {
    case t?.isNumberLiteral(): {
      return Number(t?.getText());
    }
    case t?.isBooleanLiteral(): {
      return t?.getText() === "true";
    }
    default: {
      return t?.getText();
    }
  }
}

function handleNotPrimitive(t?: Type) {
  // Here the important part. Record is object but returns {}.
  // I want to handle Records here.
  // I did not include tuples or arrays for simplicity

  if (t?.isObject()) {
    return handleObject(t);
  }

  return "Type not handled";
}

function handleObject(t?: Type) {
  const obj: Record<string, unknown> = {};
  t?.getProperties().forEach((prop) => {
    const name = prop?.isOptional() ? prop?.getName() + "?" : prop?.getName();
    const innerDeclaration = prop.getDeclarations();
    innerDeclaration.forEach((p) => {
      const innerType = p.getType();
      obj[name] = handleTypes(innerType);
    });
  });
  return obj;
}

How can I extract the types inside a record?