如何查看 TypeScript 计算类型的过程?

51
问题:我正在处理一个文件,其中包含许多条件类型,这些类型从先前定义的条件类型中派生其类型,这使得派生最终类型的路径变得非常复杂和难以调试。
我正在尝试找到一种方法来“调试”或列出TypeScript编译器如何确定条件类型并选择派生最终类型的路径。
我已经查看了编译器选项,但在那个区域里还没有找到任何东西...
现在我正在寻找的类比于DEBUG=express:*类型的设置,如果您想要查看express服务器正在执行什么操作,可以使用该设置。
然而,我现在要解决的实际问题是能够分解和调试在大型复杂的分层类型定义中如何派生类型。
重要注意事项:我不是在尝试调试TypeScript项目的运行时执行。我正在尝试调试TypeScript编译器如何计算类型。

只需使用一个好的IDE,实例化您的类型并将鼠标悬停在编辑器中打开的源文件中的值上。通过使用这个建议,您是否会错过一些额外想要的信息? - Patrick Roberts
2
@PatrickRoberts - 感谢回复。当我这样做时,它指向一个具有嵌套条件类型的复杂类型。这又指向另一个类似的复杂类型,并且它不断进行,有时会以不明显的方式分支。试图找出如何调试为什么发生该类型构造分支。 - Guy
2
我认为你的问题会受益于一个具体的例子来演示这个情况。我之前也遇到过你描述的情况,但通常我发现解决方法涉及重写类型,使它们更加不透明(例如使用一个自说明的容器名字作为通用接口,而不是试图在 IDE 工具提示中扩展其定义的通用类型),或者只是重构源代码以避免过度使用复杂的条件类型。 - Patrick Roberts
@PatrickRoberts 尝试将这个仓库升级到 Hapi/Joi@16 并调试类型生成导致了这个问题。https://github.com/TCMiranda/joi-extract-type - Guy
1
尝试在测试中记录您对打字的假设,这些测试会涵盖您关心的变化。这些测试不一定需要工作代码(可能只是变量声明的链),但是如果它们出现问题,则编译器在构建它们时会发出警告。这样不仅可以捕获违反假设的地方,而且还可以在未来编码和 TypeScript 版本更改期间捕获回归。 - rob3c
显示剩余2条评论
2个回答

14
没有内置机制可以在 TypeScript 中记录所需的信息。但是,如果您有兴趣了解内部工作原理,可以查看以下源代码中实际解析条件类型的位置。
请参阅 checker.ts 中的以下位置: ln:13258 instantiateTypeWorker()
ln:12303 getConditionalType()
ln:12385 getTypeFromConditionalTypeNode()
ln:12772 getTypeFromTypeNode()
这里附上一个我匆忙拼凑的 TypeScript 插件,它记录了一个ConditionalType的原始数据结构。要了解此结构,请查看types.ts ln:4634。
此插件的用户体验很糟糕,但该结构确实告诉您 TypeScript 如何决定条件类型的最终值。

import stringify from "fast-safe-stringify";

function init(modules: {
  typescript: typeof import("typescript/lib/tsserverlibrary");
}) {
  const ts = modules.typescript;

  // #region utils
  function replacer(name, val) {
    if (name === "checker" || name === "parent") {
      return undefined;
    }
    return val;
  }

  function getContainingObjectLiteralElement(node) {
    var element = getContainingObjectLiteralElementWorker(node);
    return element &&
      (ts.isObjectLiteralExpression(element.parent) ||
        ts.isJsxAttributes(element.parent))
      ? element
      : undefined;
  }

  ts.getContainingObjectLiteralElement = getContainingObjectLiteralElement;
  function getContainingObjectLiteralElementWorker(node) {
    switch (node.kind) {
      case 10 /* StringLiteral */:
      case 14 /* NoSubstitutionTemplateLiteral */:
      case 8 /* NumericLiteral */:
        if (node.parent.kind === 153 /* ComputedPropertyName */) {
          return ts.isObjectLiteralElement(node.parent.parent)
            ? node.parent.parent
            : undefined;
        }
      // falls through
      case 75 /* Identifier */:
        return ts.isObjectLiteralElement(node.parent) &&
          (node.parent.parent.kind === 192 /* ObjectLiteralExpression */ ||
            node.parent.parent.kind === 272) /* JsxAttributes */ &&
          node.parent.name === node
          ? node.parent
          : undefined;
    }
    return undefined;
  }

  function getPropertySymbolsFromContextualType(
    node,
    checker,
    contextualType,
    unionSymbolOk
  ) {
    var name = ts.getNameFromPropertyName(node.name);
    if (!name) return ts.emptyArray;
    if (!contextualType.isUnion()) {
      var symbol = contextualType.getProperty(name);
      return symbol ? [symbol] : ts.emptyArray;
    }
    var discriminatedPropertySymbols = ts.mapDefined(
      contextualType.types,
      function(t) {
        return ts.isObjectLiteralExpression(node.parent) &&
          checker.isTypeInvalidDueToUnionDiscriminant(t, node.parent)
          ? undefined
          : t.getProperty(name);
      }
    );
    if (
      unionSymbolOk &&
      (discriminatedPropertySymbols.length === 0 ||
        discriminatedPropertySymbols.length === contextualType.types.length)
    ) {
      var symbol = contextualType.getProperty(name);
      if (symbol) return [symbol];
    }
    if (discriminatedPropertySymbols.length === 0) {
      // Bad discriminant -- do again without discriminating
      return ts.mapDefined(contextualType.types, function(t) {
        return t.getProperty(name);
      });
    }
    return discriminatedPropertySymbols;
  }
  ts.getPropertySymbolsFromContextualType = getPropertySymbolsFromContextualType;

  function getNodeForQuickInfo(node) {
    if (ts.isNewExpression(node.parent) && node.pos === node.parent.pos) {
      return node.parent.expression;
    }
    return node;
  }
  // #endregion

  /**
   * plugin code starts here
   */
  function create(info: ts.server.PluginCreateInfo) {
    const log = (s: any) => {
      const prefix =
        ">>>>>>>> [TYPESCRIPT-FOOBAR-PLUGIN] <<<<<<<< \n";
      const suffix = "\n<<<<<<<<<<<";
      if (typeof s === "object") {
        s = stringify(s, null, 2);
      }
      info.project.projectService.logger.info(prefix + String(s) + suffix);
    };

    // Diagnostic logging
    log("PLUGIN UP AND RUNNING");

    // Set up decorator
    const proxy: ts.LanguageService = Object.create(null);
    for (let k of Object.keys(info.languageService) as Array<
      keyof ts.LanguageService
    >) {
      const x = info.languageService[k];
      proxy[k] = (...args: Array<{}>) => x.apply(info.languageService, args);
    }

    proxy.getQuickInfoAtPosition = (filename, position) => {
      var program = ts.createProgram(
        [filename],
        info.project.getCompilerOptions()
      );
      var sourceFiles = program.getSourceFiles();
      var sourceFile = sourceFiles[sourceFiles.length - 1];
      var checker = program.getDiagnosticsProducingTypeChecker();
      var node = ts.getTouchingPropertyName(sourceFile, position);
      var nodeForQuickInfo = getNodeForQuickInfo(node);
      var nodeType = checker.getTypeAtLocation(nodeForQuickInfo);

      let res;
      if (nodeType.flags & ts.TypeFlags.Conditional) {
        log(stringify(nodeType, replacer, 2));
      }

      if (!res)
        res = info.languageService.getQuickInfoAtPosition(filename, position);
      return res;
    };

    return proxy;
  }

  return { create };
}

export = init;

以下是让这个插件运行的烦人详细说明:

  1. mkdir my-ts-plugin && cd my-ts-plugin
  2. touch package.json并写入 { "name": "my-ts-plugin", "main": "index.js" }
  3. yarn add typescript fast-safe-stringify
  4. 复制并粘贴此片段到 index.ts,使用 tsc 编译它以生成 index.js
  5. yarn link
  6. 现在 cd 到您自己的 ts 项目目录,运行 yarn link my-ts-plugin
  7. { "compilerOptions": { "plugins": [{ "name": "my-ts-plugin" }] } } 添加到您的 tsconfig.json
  8. 在工作区设置中添加(.vscode/settings.json) 这一行:{ "typescript.tsdk": "<PATH_TO_YOUR_TS_PROJECT>/node_modules/typescript/lib" }
  9. 打开 vscode 命令面板并运行:
    1. TypeScript: Select TypeScript Version... -> Use Workspace Version
    2. TypeScript: Restart TS Server
    3. TypeScript: Open TS Server Log
  10. 您应该能够看到插件记录输出 "PLUGIN UP AND RUNNING",现在打开一个 ts 代码文件并将鼠标悬停在某个条件类型节点上,您应该会看到一个非常长的 json 数据结构添加到日志文件中。

感谢@hackape的帮助。我一直在尝试破解它,并能够生成一些有趣的日志,几乎列出了我在使用VSCode时交互式看到的内容,所以它并没有让我比之前更进一步。关于如何让插件工作的说明很好。 - Guy
我把悬赏给了你。虽然它没有帮我找到解决方案,但我认为如果我在修改那个插件方面再付出更多努力,我可能会找到答案,而且我想在不久的将来也不会有更好的解决方案。感谢你的帮助和努力! - Guy
我不会将此标记为正确答案,以防未来有人提供精确的解决方案。 - Guy
1
@Guy 感谢赏金。所以昨天我又花了几个小时尝试获取更多有用的结果。你说得对。上面的数据结构捕捉到了条件类型链的AST-ish对象,但这只是解析结果,而不是评估结果。至于“为什么”或者类型解析器在评估时选择哪个条件分支,则需要转储所有中间步骤的结果,有点像在某个地方放置“debugger”来暂停,然后手动查看调用堆栈中的局部域。 - hackape
1
我修改了checker.ts中的getConditionalType()函数,以制作一个自定义的TypeScript版本,并插入了一些副作用逻辑来在过程中输出中间信息。这次我得到了一些更有用的东西。我会整理我的代码并稍后附上一个gist。 - hackape
2
@Guy 这里是要点概述 链接 - hackape

4

我正在使用的一个技巧是,在使用条件类型时,将值替换为字符串字面量以确定所采取的路径。

假设你有以下代码:

type SomeComplexType<T> = T extends string ? /* Condition A */ : /* Condition B */

条件A条件B是两个深度嵌套的路径,其中包含其他条件时,我将其替换为:

type SomeComplexType<T> = T extends string ? "string" : "noString"

然后当使用它时:

type result = SomeComplexType<"myComplexType">

你可以将鼠标悬停在result上,它会显示SomeComplexType的解析值,这种情况下可能是"string""noString"。 一旦你知道了解析值,就可以用原始条件替换它,并在更深层次上执行相同的步骤。 虽然不是理想的方法,但总比没有好。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接