使用ts-morph自动化生成TypeScript接口API文档

作者:拾光

时间:2026-10-26

浏览:362

本文介绍如何利用ts-morph库解析TypeScript接口(interface)的抽象语法树,自动提取属性名、类型及JSDoc注释,并转换为适合React表格或Markdown渲染的数据结构。相比Babel方案,该方法能更直接地获取类型信息与文档注释,简化了前端组件库的API文档自动化生成流程。

文章目录

前言

近期在开发React组件库时,完成了利用抽象语法树(AST)将TypeScript接口转换为对象或数组的技术方案。转换后的数据可直接用于渲染React组件中的表格或Markdown表格。具体实现逻辑如下文所示。

UI层面

以下是一个经过转换处理的接口示例。

export interface TdAffixProps {
  /**
   * 指定滚动的容器。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body
   * @default () => (() => window)
   */
  container: any;
  /**
   * @desc 距离容器顶部达到指定距离后触发固定
   * @default 0
   */
  offsetBottom?: number;
  /**
   * @desc 距离容器底部达到指定距离后触发固定
   * @default 0
   */
  offsetTop?: number;
  /**
   * @desc 固钉定位层级,样式默认为 500
   */
  zIndex?: number;
  /**
   * @desc 固定状态发生变化时触发
   */
  onFixedChange?: (affixed: boolean, context: { top: number }) => void;
}

转换后的结构大致如下。

前言

数据层面

接口被转换为数组,数组中每一项包含所需字段,可直接传递给表格组件进行渲染。若需生成Markdown格式,只需将该数组转换为Markdown表格即可,实现难度较低。

{
  "name": "TdAffixProps",
  "data": [
    {
      "name": "container",
      "type": "any",
      "jsdoc": {
        "kind": 24,
        "description": "指定滚动的容器。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body",
        "tags": [
          {
            "kind": 25,
            "tagName": "default",
            "text": "() => (() => window)"
          }
        ]
      }
    },
    {
      "name": "offsetBottom",
      "type": "number",
      "isOptionnal": "?",
      "jsdoc": {
        "kind": 24,
        "description": "",
        "tags": [
          {
            "kind": 25,
            "tagName": "desc",
            "text": "距离容器顶部达到指定距离后触发固定"
          },
          {
            "kind": 25,
            "tagName": "default",
            "text": "0"
          }
        ]
      }
    },
    {
      "name": "offsetTop",
      "type": "number",
      "isOptionnal": "?",
      "jsdoc": {
        "kind": 24,
        "description": "",
        "tags": [
          {
            "kind": 25,
            "tagName": "desc",
            "text": "距离容器底部达到指定距离后触发固定"
          },
          {
            "kind": 25,
            "tagName": "default",
            "text": "0"
          }
        ]
      }
    },
    {
      "name": "zIndex",
      "type": "number",
      "isOptionnal": "?",
      "jsdoc": {
        "kind": 24,
        "description": "",
        "tags": [
          {
            "kind": 25,
            "tagName": "desc",
            "text": "固钉定位层级,样式默认为 500"
          }
        ]
      }
    },
    {
      "name": "onFixedChange",
      "type": "(affixed: boolean, context: { top: number }) => void",
      "isOptionnal": "?",
      "jsdoc": {
        "kind": 24,
        "description": "",
        "tags": [
          {
            "kind": 25,
            "tagName": "desc",
            "text": "固定状态发生变化时触发"
          }
        ]
      }
    }
  ]
}

我们需要的数据结构

由上述过程可知,目标数据结构应包含以下字段。

{
    name: xxx, // interface的名字,
    data: [
        {
          name: xx, // interface里每一项的属性名
          type: xx,  // interface里每一项的类型
          isOptionnal: xx, // 是否是可选项
          jsDoc: {} // 后面细说
        }
    ]
}

简单解释一下jsdoc格式

JSDoc是一种文档生成工具,用于为JavaScript代码生成API文档。它通过特定的注释格式来描述代码中类型、函数、变量等的用途、参数及返回值等信息。

例如,在JavaScript代码中可以使用如下注释来描述一个函数。

/**
 * 描述文字
 * @default 0 
 */
function sum(x, y) {
  return x + y;
}

该注释被解析后的结构如下。

   {
        "kind": 24, // 忽略
        "description": "描述文字",
        "tags": [
          {
            "kind": 25, // 忽略
            "tagName": "default",
            "text": 0
          }
        ]
      }

AST解析技术选择

为什么放弃babel

起初仅了解Babel,因长期使用Webpack而对其他AST相关前端库不熟悉,故初期采用了Babel方案,但随后发现了以下问题。

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const generate = require('@babel/generator').default
const fs = require("fs")

fs.readFile('./type.ts', { encoding: 'utf-8' }, function (err, data) {
  if (err) throw err;
  const result = [];
  const ast = parser.parse(data, {
    sourceType: "unambiguous",
    plugins: ["typescript"]
  });
  traverse(ast, {
    TSInterfaceDeclaration(path) {
      path.traverse({
        TSPropertySignature(path) {
          console.log(path.node.key.name);
          console.log(path.node.leadingComments?.[0]?.value);
        },
      });
    }
  });

});

例如,在打印节点时,number类型的节点类型标识为TSNumberKeyword。但我们的目标并非获取TSNumberKeyword,而是获取number类型,该如何处理?

有人可能会说简单,做个映射即可。

{
  TSNumberKeyword: "number"
}

确实,简单的映射可以实现,但面对function类型时,映射变得复杂。虽然可以考虑直接使用generator将类型片段还原,但这种方法显得不够优雅。

此外,无法直接获取JSDoc中的类型信息,因为注释本质上是字符串,需要自行处理才能转换为JSDoc格式。

因此,我查阅了Arco CLI中使用的转换库ts-morph,发现该库非常适合此需求,下文将详细介绍。

顺便提一句,我的实现比字节团队的Arco CLI要简单非常多。

ts-morph

该库极大地降低了不熟悉TypeScript繁琐底层类型和方法的开发者的使用门槛。ts-morph是一个专门针对TypeScript/JavaScript的AST处理库,可用于浏览和修改TS/JS的抽象语法树。

关于ts-morph的详细文档,可参见其官网:ts-morph.com/。

以下是我的基本实现思路(可将内部函数抽取为中间件以提高可维护性,目前暂未重构,类型定义也未完全完善,读者可在此基础上封装适合自身业务的代码,整体思路清晰),后续计划将其封装为独立库供React组件库使用。

以下代码核心仅为一个简单函数。Arco官方CLI工具代码约200行,但复杂度远高于此方案。

自动化生成代码

import { Project } from "ts-morph";

const internalProject = new Project({
  tsConfigFilePath: "./tsconfig.json",
});

const sourceFile = internalProject.getSourceFile("./type.ts");
const interfaces = sourceFile!.getInterfaces();

const result:any[] = [];
interfaces.forEach((inter_face)=>{
  result.push({
    name: '',
    data: []
  });
  const index = result.length - 1;
  result[index].name = inter_face.getName();

  inter_face.getProperties().forEach((v) => {
    result[index].data.push({
      name: v.getName(),
      type: v.getTypeNode()?.getText(),
      isOptionnal: v.getQuestionTokenNode()?.getText(),
      jsdoc:v.getJsDocs().map((jsDoc)=>{
        return (jsDoc.getStructure())
       })[0]
    });    
  });
})
console.log(result);

总结

至此,关于TypeScript中interface自动化生成API文档的介绍结束。如需了解更多TypeScript自动化生成API文档相关内容,请搜索本站往期文章或继续浏览相关文章,感谢支持。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章