Generate TS Interface

Generate TS interfaces from YApi with robust nested array and anonymous node handling (I prefix, remove List, skip method prefix)

Versión del día 20/10/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Generate TS Interface
// @namespace    http://tampermonkey.net/
// @version      1.8.0
// @description  Generate TS interfaces from YApi with robust nested array and anonymous node handling (I prefix, remove List, skip method prefix)
// @author       ihopeful
// @match        *://*/project/*/interface/api/*
// @grant        GM_setClipboard
// @icon         https://yapi.pro/image/favicon.png
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const typeMap = {
    string: 'string',
    number: 'number',
    integer: 'number',
    boolean: 'boolean',
    object: 'Record<string, any>',
    'object []': 'Record<string, any>[]',
    array: 'any[]'
  };

  const toTsType = t => {
    if (!t) return 'any';
    const lower = t.toLowerCase();
    if (lower.includes('array')) return 'any[]';
    if (lower.includes('object')) return 'Record<string, any>';
    return typeMap[lower] || 'any';
  };

  const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);

  const ignoreNames = ['List', 'Data', 'Response'];
  let anonCount = 1;
  const interfaces = [];

  /** 自动展开所有折叠行 */
  function expandAll(callback) {
    const icons = Array.from(document.querySelectorAll('span.ant-table-row-expand-icon'))
      .filter(el => el.className.includes('ant-table-row-collapsed'));
    if (icons.length === 0) {
      if (callback) callback();
      return;
    }
    icons.forEach(icon => {
      icon.scrollIntoView();
      icon.click();
    });
    setTimeout(() => expandAll(callback), 500);
  }

  /** 安全生成接口名 */
  function safeInterfaceName(name, parentName, isArray) {
    let base;

    if (!name) {
      // 匿名对象用父名 + DTO
      base = `${parentName.replace(/^I/, '')}DTO`;
    } else {
      base = capitalize(name);
      if (base.endsWith('List')) base = base.slice(0, -4);
    }

    let interfaceName = 'I' + base;

    // 重名检测
    if (interfaces.some(i => i.startsWith(`interface ${interfaceName} `))) {
      interfaceName += anonCount++;
    }

    return interfaceName;
  }

  /** 解析请求参数 */
  function parseRequest(prefix) {
    const rows = document.querySelectorAll('.colQuery table tbody tr');
    if (!rows.length) return null;

    const props = Array.from(rows).map(tr => {
      const tds = tr.querySelectorAll('td');
      const name = tds[0]?.innerText.trim();
      const required = (tds[1]?.innerText || '').includes('是');
      const comment = tds[3]?.innerText.trim() || name;
      const type = 'string | number';
      return `  /** ${comment} */\n  ${name}${required ? '' : '?'}: ${type};`;
    });

    return `interface ${prefix}RequestQuery {\n${props.join('\n')}\n}`;
  }

  /** 解析响应参数 */
  function parseResponse(prefix) {
    const rows = document.querySelectorAll('h2.interface-title + div table tbody tr');
    if (!rows.length) return null;

    const root = { name: 'Response', children: [] };
    const stack = [root];

    Array.from(rows).forEach(tr => {
      const levelClass = Array.from(tr.classList).find(c => c.includes('ant-table-row-level-'));
      const level = levelClass ? parseInt(levelClass.split('-').pop(), 10) : 0;
      const tds = tr.querySelectorAll('td');
      const name = tds[0]?.innerText.trim();
      const typeRaw = tds[1]?.innerText.trim();
      const required = (tds[2]?.innerText || '').includes('必须');
      const comment = tds[4]?.innerText.trim() || name;
      const type = toTsType(typeRaw);

      const node = { name, type, required, comment, children: [], rawType: typeRaw || '' };
      while (stack.length > level + 1) stack.pop();
      stack[stack.length - 1].children.push(node);
      stack.push(node);
    });

   function genInterface(nodes, parentName, rootPrefix = parentName.replace(/^I/, '')) {
  let res = '';

  nodes.forEach(n => {
    const pad = '  ';
    const comment = `/** ${n.comment || ''} */\n`;
    const isArray = n.rawType.includes('[]');
    const hasChildren = n.children.length > 0;

    if (hasChildren) {
      // ✅ 若为数组 + 匿名对象子项 → 提升为 DTO[]
      if (isArray && n.children.length === 1 && (!n.children[0].name || n.children[0].name === '')) {
        const child = n.children[0];
        const dtoName = `I${rootPrefix}${capitalize(n.name.replace(/List$/, ''))}DTO`;
        interfaces.push(`interface ${dtoName} {\n${genInterface(child.children, dtoName, rootPrefix)}\n}`);
        n.type = `${dtoName}[]`;
        res += `${pad}${comment}${n.name}${n.required ? '' : '?'}: ${n.type};\n`;
      } else {
        // ✅ 子对象用 rootPrefix + 当前字段名 生成
        const interfaceName = `I${rootPrefix}${capitalize(n.name.replace(/List$/, ''))}`;
        interfaces.push(`interface ${interfaceName} {\n${genInterface(n.children, interfaceName, rootPrefix)}\n}`);
        n.type = interfaceName + (isArray ? '[]' : '');
        res += `${pad}${comment}${n.name}${n.required ? '' : '?'}: ${n.type};\n`;
      }
    } else {
      res += `${pad}${comment}${n.name}${n.required ? '' : '?'}: ${n.type};\n`;
    }
  });

  return res.trim();
}
    interfaces.push(`interface ${prefix}Response {\n${genInterface(root.children, prefix)}\n}`);
    return interfaces.join('\n\n');
  }

  /** 获取接口名和 URL */
  function parseInterfaceName() {
    const nameEl = document.querySelector('.panel-view .colName');
    return nameEl?.innerText.trim() || '接口名称';
  }

  function parseApiUrl() {
    const urlEl = document.querySelector('.panel-view .colValue .colValue:nth-child(2)');
    if (!urlEl) return { method: 'get', url: '' };
    const method = urlEl.previousElementSibling?.innerText.toLowerCase() || 'get';
    let url = urlEl.innerText.trim();
    url = url.replace(/^\/admin/, '');
    return { method, url };
  }

  /** 生成函数名(去掉method前缀) */
  function genFuncName(url) {
    return url
      .split('/')
      .filter(Boolean)
      .map((s, i) => (i === 0 ? s.toLowerCase() : capitalize(s)))
      .join('');
  }

  /** 生成并插入 TS Interface */
  function generateInterface() {
    const apiName = parseInterfaceName();
    const { method, url } = parseApiUrl();
    const funcName = genFuncName(url);
    const prefix = 'I' + capitalize(funcName);

    const req = parseRequest(prefix);
    const res = parseResponse(prefix);

    if (!res) return;

    const paramsStr = req ? `params: ${prefix}RequestQuery` : 'params?: Record<string, any>';
    const func = `
/**
 * ${apiName}
 */
export async function ${funcName}(${paramsStr}): Promise<${prefix}Response> {
  return await ${method}(\`${url}\`, ${req ? 'params' : 'params'});
}
`;

    const result = [req, res, func].filter(Boolean).join('\n\n');

    const panelView = document.querySelector('.panel-view');
    if (panelView) {
      const pre = document.getElementById('ts-interface-generate');
      if (pre) pre.remove();
      const preEl = document.createElement('pre');
      preEl.id = 'ts-interface-generate';
      preEl.style = 'background:#f0f0f0;padding:10px;border-radius:6px;margin:10px 0;white-space:pre-wrap;position:relative;';
      preEl.textContent = result;

      const btn = document.createElement('button');
      btn.textContent = '复制';
      btn.style = 'position:absolute;top:5px;right:5px;padding:2px 6px;font-size:12px;cursor:pointer;';
      btn.onclick = () => {
        GM_setClipboard(result);
        alert('复制成功✅');
      };
      preEl.appendChild(btn);

      panelView.parentNode.insertBefore(preEl, panelView.nextSibling);
    }
  }

  // 自动展开并生成
  setTimeout(() => expandAll(generateInterface), 1500);
})();