Let's take typescript file:
class A {
private x? = 0;
private y? = 0;
f() {
console.log(this.x, this.y);
delete this.x;
}
}
const a = new A();
a.f();
I'm building it in webpack using awesome-typescript-loader:
{
test: /\.tsx?$/,
include: path.resolve("./src"),
exclude: path.resolve("./node_modules/"),
use: {
loader: 'awesome-typescript-loader',
options: {
getCustomTransformers: program => ({
before: [deleteTransformer(program)]
})
}
}
},
Where deleteTransformer
is my own transformer which replaces any delete
expression by 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;
}
If I run compilation I will get the code I expect (deletes y
, not 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();
/***/ }),
But if I change the name y
to z
which does not exist on class A
I won't get any error message.
Also if I change class A
to have nonoptional x
and keep y
in transformer, I'll get an error
× 「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.
According to these facts I understand that transformer is applied after the code is actually checked, but transformer is included into before
section, so I expect typescript to validate generated code instead of original.
Why does it happen? What are the differences of before
and after
transformers in getCustomTransformers
object (I tried both and found no difference)? And how can I apply transformations before code is checked?
At a high level, the TypeScript compiler was designed to do the following steps in the following order:
Due to this design, the type checker code often assumes the AST created in parsing matches the source file text and hasn't changed.
For example:
So, you cannot reliably only transform an AST then type check using the existing TypeScript Compiler API code. That's one of reasons why ts-morph actually makes modifications to the text instead of the AST then rebuilds the AST. The source file text and a lot of internal properties need to be updated to do this properly. That said, you might be able to get away with it in certain scenarios...
I'm not sure what kind of effort it would take for the TS team to update the compiler to handle transforms before type checking and I'm not sure it's something they'd invest effort in, but you may want to talk to them and ask about that. See in checker.ts all the calls that lead to
getTextOfNodeFromSourceText
for a bunch of cases where this would be a problem.Difference between
before
andafter
ingetCustomTransformers
As you noticed, both of these transforms are used while emitting and not before.
before
- Transformations to evaluate before the compiler does its transformations—it will still have TypeScript code in the AST.after
- Transformations to evaluate after the compiler does its transformations—it will be transformed to whatever the "target" is (ex. printing the AST will give JavaScript code).See the type declarations for more details.