You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用ts-morph从TypeScript Record中提取内部类型并生成类型树

处理ts-morph中Record类型的内部类型提取问题

问题背景

使用bun和ts-morph工具从TypeScript文件提取类型生成类型树时,能正常处理基本类型和普通对象类型,但遇到Record类型时无法获取其内部的键类型和值类型,导致无法生成完整的类型树结构。

期望测试用例

需要让代码通过以下测试(注:原测试用例中MyRecord定义修正为Record<string, Value>以匹配期望结果):

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, Value>;
`;
  const result = createTypeTree("temp.ts", sourceCode);
  expect(result).toStrictEqual({
    MyRecord: { ["string"]: { a: "number", b: "string" } },
    Id: "string",
    Value: { a: "number", b: "string" },
  });
});

原可复现代码

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;
}

解决方案

要提取Record类型的内部类型,需要识别当前类型为Record泛型类型,然后获取其泛型参数(键类型和值类型),再对值类型进行递归处理。修改handleNotPrimitive函数并新增Record处理逻辑:

修改后的完整代码

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) {
  if (!t) return "Type not handled";
  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) {
  if (!t) return "Type not handled";
  switch (true) {
    case t.isNumberLiteral(): {
      return Number(t.getText());
    }
    case t.isBooleanLiteral(): {
      return t.getText() === "true";
    }
    default: {
      return t.getText();
    }
  }
}

function handleNotPrimitive(t?: Type) {
  if (!t) return "Type not handled";

  // 处理Record类型:识别泛型并提取键和值类型
  const typeSymbol = t.getSymbol();
  if (typeSymbol?.getName() === "Record") {
    const typeArgs = t.getTypeArguments();
    if (typeArgs.length === 2) {
      const keyType = typeArgs[0].getText();
      const valueType = handleTypes(typeArgs[1]);
      return { [keyType]: valueType };
    }
  }

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

  return "Type not handled";
}

function handleObject(t?: Type) {
  if (!t) return {};
  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;
}

关键修改说明

  1. 识别Record类型:通过typeSymbol?.getName() === "Record"判断当前类型是否为Record泛型类型。
  2. 提取泛型参数:使用getTypeArguments()获取Record的两个泛型参数,第一个是键类型,第二个是值类型。
  3. 递归处理值类型:对值类型调用handleTypes()进行递归处理,保持类型树的一致性。
  4. 生成Record结构:将键类型的文本作为键,处理后的类型值作为对应的值,生成符合测试用例的结构。

内容的提问来源于stack exchange,提问作者programandoconro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 07:05:57