如何使用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; }
关键修改说明
- 识别Record类型:通过
typeSymbol?.getName() === "Record"判断当前类型是否为Record泛型类型。 - 提取泛型参数:使用
getTypeArguments()获取Record的两个泛型参数,第一个是键类型,第二个是值类型。 - 递归处理值类型:对值类型调用
handleTypes()进行递归处理,保持类型树的一致性。 - 生成Record结构:将键类型的文本作为键,处理后的类型值作为对应的值,生成符合测试用例的结构。
内容的提问来源于stack exchange,提问作者programandoconro
相关产品推荐
相关产品推荐

