Transform typescript before typecheck

1k Views Asked by At

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?

1

There are 1 best solutions below

1
On BEST ANSWER

At a high level, the TypeScript compiler was designed to do the following steps in the following order:

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

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:

// `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
));

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 and after in getCustomTransformers

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.