Modify typescript prior to typechecking

Consider the following 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();

When 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)]
      })
    }
  }
},

In this configuration, the custom transformer deleteTransformer is invoked to replace any delete expression with 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;
}

After compiling, the expected code is generated where y is deleted instead of 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();


/***/ }),

However, changing the property name from y to

z</code, which does not exist on class <code>A
, does not produce an error message.

If you modify class A to have a non-optional x property and keep y in the transformer, an error occurs during compilation:

× 「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.

These observations lead to the understanding that the transformer is applied after the code is checked. Despite being included in the before section, TypeScript validates the original code rather than the transformed code.

If you're wondering about the differences between before and after transformers in the getCustomTransformers object, testing both showed no noticeable distinction. To apply transformations before code checking occurs, further investigation into TypeScript's transformation process may be necessary.

Answer №1

When it comes to the TypeScript compiler, it follows a specific sequence of steps: Parse -> Bind -> Type Check -> Emit (transform).

This design means that the type checker code relies heavily on the assumption that the Abstract Syntax Tree (AST) generated during parsing accurately reflects the source file text and remains unchanged.

For instance:

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

// this leads to unreliable type checking
console.log(typeChecker.typeToString(
    typeChecker.getTypeAtLocation(declaration) // still number
));
console.log(typeChecker.typeToString(
    typeChecker.getTypeAtLocation(declaration.type!) // any
));

Hence, simply transforming an AST and then performing type checking using existing TypeScript Compiler API code is not entirely reliable. This is one reason why ts-morph opts for modifying the text rather than the AST itself, followed by rebuilding the AST. Properly updating the source file text and various internal properties is crucial in such cases. However, there may be exceptions...

If you wish for the TS team to consider implementing transformations before type checking, it's uncertain how much effort it would require or if they would prioritize it. Consider reaching out to them for further insights. Take a look at checker.ts to understand all the calls leading to getTextOfNodeFromSourceText for instances where issues may arise.

Distinguishing "before" from "after" in getCustomTransformers

You observed that both these transforms are applied during emission rather than beforehand.

  • before - Transformations executed prior to the compiler's own transformations, retaining TypeScript code within the AST.
  • after - Transformations carried out after the compiler's transformations, resulting in the output matching the specified "target" language (e.g., JavaScript when printing the AST).

Refer to the type declarations for more detailed information.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

encountering the issue of not being able to assign a parameter of type 'string | undefined' to a parameter of type

Seeking help with the following issue: "Argument of type 'string | undefined' is not assignable to parameter of type" I am unsure how to resolve this error. Here is the section of code where it occurs: export interface IDropDown { l ...

Modify every audio mixer for Windows

Currently working on developing software for Windows using typescript. Looking to modify the audio being played on Windows by utilizing the mixer for individual applications similar to the built-in Windows audio mixer. Came across a plugin called win-audi ...

What is the best way to prevent jest.mock from being hoisted and only use it in a single jest unit test?

My goal is to create a mock import that will be used only in one specific jest unit test, but I am encountering some challenges. Below is the mock that I want to be restricted to just one test: jest.mock("@components/components-chat-dialog", () ...

How can I display data both as a dropdown and an autocomplete in Angular using a textbox?

There is a textbox with autocomplete functionality. When the user clicks on the textbox, an API call is made with two parameters - Pubid and Date. The data is then displayed in a dropdown with autocomplete feature. Now I am attempting to have the data app ...

Encountering build:web failure within npm script due to TypeScript

Our project is utilizing the expo-cli as a local dependency in order to execute build:web from an npm script without requiring the global installation of expo-cli. However, when we run npm run build:web, we encounter the following exception. To isolate th ...

How to change a specific value in an array of objects using React

Within my array, I have objects containing the fields id and title const cols = [ { id: 0, title: "TODO" }, { id: 1, title: "InProgress" }, { id: 2, title: "Testing" }, { ...

A step-by-step guide on configuring data for aria's autocomplete feature

Currently, I am implementing aria autocomplete and facing an issue while trying to populate data from the server into the selection of aria autocomplete. I have tried setting the selected property of the aria autocomplete object, but it doesn't seem t ...

With TypeScript, you have the flexibility to specify any data type in the generic types when using the axios.get method

axios.get('/api') When working with TypeScript as shown above, it is important to designate types for better clarity. This allows us to reference the type definition of axios, like so: (method) AxiosInstance.get<any, AxiosResponse<any> ...

Using the css function within styled-components

Struggling with implementing the media templates example from the documentation and figuring out how to type the arguments for the css function in plain JS: const sizes = { desktop: 992 } const media = Object.keys(sizes).reduce((acc, label) => { ...

Struggling with TypeScript declaration files has been a challenge for me

I'm encountering an issue with using the trace function in my TypeScript code. The function has been declared in a .d.ts file as shown below: declare function trace(arg: string | number | boolean); declare function trace(arg: { id: number; name: strin ...

Learn how to automatically display a modal upon loading a webpage by simply entering the URL of the specific template

Is there a way to trigger the modal pop-up by simply typing a URL link without the need for any click function? I am currently attempting to display the modal without requiring a login, but when I type the URL, the modal appears briefly and then disappears ...

Is it possible in Typescript to assign a type to a variable using a constant declaration?

My desired outcome (This does not conform to TS rules) : const type1 = "number"; let myVariable1 : typeof<type1> = 12; let type2 = "string" as const; let myVariable2 : typeof<type2> = "foo"; Is it possible to impl ...

Developing a bespoke React Typescript button with a custom design and implementing an onClick event function

Currently, I am in the process of developing a custom button component for a React Typescript project utilizing React Hooks and Styled components. // Button.tsx import React, { MouseEvent } from "react"; import styled from "styled-components"; export int ...

What is the best way to generate a dummy ExecutionContext for testing the CanActivate function in unit testing?

In my authGuard service, I have a canActivate function with the following signature: export interface ExecutionContext extends ArgumentsHost { /** * Returns the *type* of the controller class which the current handler belongs to. */ get ...

How can a TypeScript Type be handed over as a prop to a React component?

Can you pass a TypeScript type as a property to a React Component? export type ActivitiesType = { RUN: "RUN"; WALK: "REST"; ROUNDS: "ROUNDS"; }; <MyComponent activity={ActivitiesType.RUN} /> Next, in MyComponent: const MyComponent = ({ act ...

Understanding the fundamentals of TypeScript annotation and node package management

As a newcomer to Typescript, I have grasped the basics but find myself becoming a bit bewildered when it comes to best practices for handling node packages, annotations, and defining types within those packages in my projects. Do I really need to annotate ...

Tips on leveraging an attribute for type guarding in a TypeScript class with generics

Is there a way to utilize a generic class to determine the type of a conditional type? Here is a basic example and link to TS playground. How can I access this.b and this.a without relying on as or any manual adjustments? type X<T> = T extends true ...

What is the best way to display data retrieved through an HTTP service using ngFor?

I was attempting to inject a service (contact.service.ts) into a component (contact-list.component). The service contains data on employees defined in contacts.ts. While I was able to successfully receive the data, I encountered difficulty in rendering it ...

Problem with rendering Ionic v2 HTML in Angular 2

Issue at Hand: Currently, I am developing a hybrid app using Ionic v2 and everything seems to be functioning correctly. When I use ionic serve, the app works as expected in the browser with no issues. However, when I try running the app on an Android devi ...

Running out of memory due to inefficient mark-compacting processes nearing the heap limit in Angular 8 allocation

A significant portion of the modules are built, with only one active in progress. The process is located at ...\src\index.js??extracted!D:\Clients\app\node_modules\sass-loader\lib\loader.js??ref--15-3!D:\src&bso ...