在类型检查之前转换打字稿

Transform typescript before typecheck

让我们使用打字稿文件:

class A {
    private x? = 0;
    private y? = 0;

    f() {
        console.log(this.x, this.y);
        delete this.x;
    }
}

const a = new A();
a.f();

我正在使用 awesome-typescript-loader:

在 webpack 中构建它
{
  test: /\.tsx?$/,
  include: path.resolve("./src"),
  exclude: path.resolve("./node_modules/"),
  use: {
    loader: 'awesome-typescript-loader',
    options: {
      getCustomTransformers: program => ({ 
        before: [deleteTransformer(program)]
      })
    }
  }
},

其中 deleteTransformer 是我自己的转换器,它将任何 delete 表达式替换为 delete this.y:

import * as ts from "typescript";

export default function getCustomTransformers(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
  return (context: ts.TransformationContext) => (file: ts.SourceFile) => visitNodeAndChildren(file, program, context);
}

function visitNodeAndChildren<N extends ts.Node>(node: N, program: ts.Program, context: ts.TransformationContext): N {
  return ts.visitEachChild(visitNode(node, program), childNode => visitNodeAndChildren(childNode, program, context), context);
}

function visitNode<N extends ts.Node>(node: N, program: ts.Program): N {
  if (ts.isDeleteExpression(node)) {
    return ts.factory.createDeleteExpression(ts.factory.createPropertyAccessExpression(
      ts.factory.createThis(),
      "y",
    )) as ts.Node as N;
  }

  return node;
}

如果我 运行 编译我会得到我期望的代码(删除 y,而不是 x):

/***/ "/7QA":
/***/ (function(module, exports, __webpack_require__) {

"use strict";

var A = /** @class */ (function () {
    function A() {
        this.x = 0;
        this.y = 0;
    }
    A.prototype.f = function () {
        console.log(this.x, this.y);
        delete this.y;
    };
    return A;
}());
var a = new A();
a.f();


/***/ }),

但是如果我将名称 y 更改为 z class A 上不存在的名称,我将不会收到任何错误消息。

此外,如果我将 class A 更改为具有非可选 x 并在 transformer 中保留 y,我将收到错误消息

× 「atl」: Checking finished with 1 errors

ERROR in [at-loader] ./src/index.ts:7:16
    TS2790: The operand of a 'delete' operator must be optional.

根据这些事实,我了解到 transformer 是在实际检查代码后应用的 ,但是 transformer 包含在 before 部分中,所以我希望打字稿能够验证生成代码而不是原始代码。

为什么会这样? beforeafter transformers 在 getCustomTransformers object 中有什么区别(我都试过了,没有发现区别)?在检查 代码之前,如何应用转换

在高层次上,TypeScript 编译器被设计为按以下顺序执行以下步骤:

Parse -> Bind -> Type Check -> Emit (transform)

由于这种设计,类型检查器代码通常假设在解析中创建的 AST 与源文件文本匹配并且没有改变。

例如:

// `declaration` is a variable declaration with type `number`
console.log(typeChecker.typeToString(
    typeChecker.getTypeAtLocation(declaration) // number
));

declaration = factory.updateVariableDeclaration(
    declaration,
    declaration.name,
    /* exclamation token */ undefined,
    /* type */ factory.createTypeReferenceNode("Date", undefined),
    /* initializer */ undefined,
);

// now type checking won't be reliable
console.log(typeChecker.typeToString(
    typeChecker.getTypeAtLocation(declaration) // still number
));
console.log(typeChecker.typeToString(
    typeChecker.getTypeAtLocation(declaration.type!) // any
));

因此,您不能可靠地只转换 AST,然后使用现有的 TypeScript 编译器 API 代码进行类型检查。这就是为什么 ts-morph 实际上对文本而不是 AST 进行修改然后重建 AST 的原因之一。需要更新源文件文本和许多内部属性才能正确执行此操作。也就是说,在某些情况下您也许可以摆脱它...

我不确定 TS 团队需要付出什么样的努力来更新编译器以在类型检查之前处理转换,我不确定他们是否会投入精力,但你可能想要与他们交谈并询问。在 checker.ts 中查看导致 getTextOfNodeFromSourceText 的所有调用,以了解一堆可能会出现问题的情况。

beforeaftergetCustomTransformers

中的区别

如您所见,这两种变换都是在发射时使用的,而不是之前使用的。

  • before - 在编译器进行转换之前要评估的转换——它仍然会在 AST 中包含 TypeScript 代码。
  • after - 在编译器进行转换后评估的转换——它将被转换为“目标”是什么(例如打印 AST 将给出 JavaScript 代码)。

有关详细信息,请参阅 the type declarations