Leverage the compiler API to perform type inference

Exploring TypeScript's compiler API for basic type inference has proven to be a challenge with limited helpful information found in documentation or online searches.

My goal is to create a function inferType that can determine and return the inferred type of a given variable:

let bar = [1, 2, 3];
let bar2 = 5;

function foo(a: number[], b: number) {
  return a[0] + b;
}

inferType(bar); // => "number[]"
inferType(bar2); // => "number"
inferType(foo); // "(number[], number) => number"

I am curious if it is possible to achieve this using the compiler API. If not, I am open to exploring alternative methods to accomplish this task.

Answer №1

Check out this TypeScript Compiler API Playground example demonstrating LanguageService type checking:

Additionally, there is a node.js script that parses input typescript code and infers the type of any symbol based on its usage. This script utilizes the TypeScript Compiler API by creating a Program and using "program.getTypeChecker().getTypeAtLocation(someNode)"

For a working example, visit: https://github.com/cancerberoSgx/typescript-plugins-of-mine/blob/master/typescript-ast-util/spec/inferTypeSpec.ts

If you are unfamiliar with the Compiler API, consider starting here and exploring these helpful projects:

Best of luck!

Answer №2

Solution 1

To achieve this, utilizing the compiler API with an emit transformer is recommended. The emit transformer receives the Abstract Syntax Tree (AST) during the compilation process and has the ability to modify it. Internally, transformers are used by the compiler to convert TypeScript AST into JavaScript AST which is then written to a file.

The approach involves creating a custom transformer that identifies a function named inferType and adds an additional argument containing the TypeScript type name to the function call.

transformation.ts

import * as ts from 'typescript'
// Defining the transformer factory
function transformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
    let typeChecker =  program.getTypeChecker();
    function transformFile(program: ts.Program, context: ts.TransformationContext, file: ts.SourceFile): ts.SourceFile {
        function visit(node: ts.Node, context: ts.TransformationContext): ts.Node {
            // Handling call expressions
            if (ts.isCallExpression(node)) {
                let target = node.expression;
                // Checking for calls to inferType function
                if(ts.isIdentifier(target) && target.escapedText == 'inferType'){
                    // Obtaining argument's type
                    var type = typeChecker.getTypeAtLocation(node.arguments[0]);
                    // Retrieving type name
                    var typeName = typeChecker.typeToString(type)
                    // Updating the call expression by adding an extra parameter
                    return ts.updateCall(node, node.expression, node.typeArguments, [
                        ... node.arguments,
                        ts.createLiteral(typeName)
                    ]);
                }
            }
            return ts.visitEachChild(node, child => visit(child, context), context);
        }
        const transformedFile = ts.visitEachChild(file, child => visit(child, context), context);
        return transformedFile;
    }
    return (context: ts.TransformationContext) => (file: ts.SourceFile) => transformFile(program, context, file);
}
// Compiling a source file
var cmd = ts.parseCommandLine(['test.ts']);
// Creating the program
let program = ts.createProgram(cmd.fileNames, cmd.options);

// Emitting the program with the custom transformer
var result = program.emit(undefined, undefined, undefined, undefined, {
    before: [
        transformer(program)
    ]
} );

test.ts

let bar = [1, 2, 3];
let bar2 = 5;

function foo(a: number[], b: number) {
return a[0] + b;
}
function inferType<T>(arg:T, typeName?: string) {
    return typeName;

}
inferType(bar); // => "number[]"
inferType(bar2); // => "number"
inferType(foo); // "(number[], number) => number"

Resulting test.js File

var bar = [1, 2, 3];
var bar2 = 5;
function foo(a, b) {
    return a[0] + b;
}
function inferType(arg, typeName) {
    return typeName;
}
inferType(bar, "number[]"); // => "number[]"
inferType(bar2, "number"); // => "number"
inferType(foo, "(a: number[], b: number) => number"); // "(number[], number) => number"

Note: This demonstration serves as a proof of concept, further testing is required. Integrating this custom transformation into your build process may pose challenges, requiring the replacement of the original compiler with this customized version for the desired transformation to take effect.

Solution 2

Another alternative involves leveraging the compiler API to introduce a transformation in the source code pre-compilation. This transformation incorporates the type name directly into the source file. Although presenting the type as a string within the source file, including this transformation in the build process ensures automatic updates. Additionally, using the original compiler and tools remains possible without any alterations.

transformation.ts

import * as ts from 'typescript'

function transformFile(program: ts.Program, file: ts.SourceFile): ts.SourceFile {
    let empty = ()=> {};
    // Placeholder transformation context
    let context: ts.TransformationContext = {
        startLexicalEnvironment: empty,
        suspendLexicalEnvironment: empty,
        resumeLexicalEnvironment: empty,
        endLexicalEnvironment: ()=> [],
        getCompilerOptions: ()=> program.getCompilerOptions(),
        hoistFunctionDeclaration: empty,
        hoistVariableDeclaration: empty,
        readEmitHelpers: ()=>undefined,
        requestEmitHelper: empty,
        enableEmitNotification: empty,
        enableSubstitution: empty,
        isEmitNotificationEnabled: ()=> false,
        isSubstitutionEnabled: ()=> false,
        onEmitNode: empty,
        onSubstituteNode: (hint, node)=>node,
    };
    let typeChecker =  program.getTypeChecker();
    function visit(node: ts.Node, context: ts.TransformationContext): ts.Node {
        // Handling call expressions
        if (ts.isCallExpression(node)) {
            let target = node.expression;
            // Looking for calls to inferType function
            if(ts.isIdentifier(target) && target.escapedText == 'inferType'){
                // Accessing argument's type
                var type = typeChecker.getTypeAtLocation(node.arguments[0]);
                // Retrieving the type name
                var typeName = typeChecker.typeToString(type)
                // Modifying the call expression to include an additional parameter
                var argument =  [
                    ... node.arguments
                ]
                argument[1] = ts.createLiteral(typeName);
                return ts.updateCall(node, node.expression, node.typeArguments, argument);
            }
        }
        return ts.visitEachChild(node, child => visit(child, context), context);
    }

    const transformedFile = ts.visitEachChild(file, child => visit(child, context), context);
    return transformedFile;
}

// Compiling a file
var cmd = ts.parseCommandLine(['test.ts']);
// Establishing the program
let host = ts.createCompilerHost(cmd.options);
let program = ts.createProgram(cmd.fileNames, cmd.options, host);
let printer = ts.createPrinter();

let transformed = program.getSourceFiles()
    .map(f=> ({ o: f, n:transformFile(program, f) }))
    .filter(x=> x.n != x.o)
    .map(x=> x.n)
    .forEach(f => {
        host.writeFile(f.fileName, printer.printFile(f), false, msg => console.log(msg), program.getSourceFiles());
    })

test.ts

let bar = [1, 2, 3];
let bar2 = 5;
function foo(a: number[], b: number) {
    return a[0] + b;
}
function inferType<T>(arg: T, typeName?: string) {
    return typeName;
}
let f = { test: "" };
// Running the provided code automatically appends/updates the type name parameter.
inferType(bar, "number[]");
inferType(bar2, "number"); 
inferType(foo, "(a: number[], b: number) => number"); 
inferType(f, "{ test: string; }");

Answer №3

Is there a way to accomplish this using the compiler API?

The compiler API allows you to examine code when it is represented as a string. For example:

const someObj = ts.someApi(`
// Code 
let bar = [1, 2, 3];
let bar2 = 5;

function foo(a: number[], b: number) {
  return a[0] + b;
}
`);
// use someObj to make inferences about the code

If not, are there any other methods to achieve this?

You can utilize typeof, although it has its limitations (source).

Another approach is to load your own code using nodejs __filename (this will only work in Node.js and only if running through ts-node with raw TypeScript): https://nodejs.org/api/globals.html#globals_filename.

Answer №4

Exploring a different approach with decorators.

function getType(target: Object, propertyKey: string): any {
}

class MyClass {
    @getType foo: number;
    @getType elem: HTMLElement;
}

console.info(Reflect.getMetadata("design:type", Object.getPrototypeOf(MyClass), "foo")); // Number constructor
console.info(Reflect.getMetadata("design:type", Object.getPrototypeOf(MyClass), "elem")); // HTMLElement constructor

Keep in mind that to make this work, certain configurations need to be enabled:

"compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
}

Additionally, use the reflect-metadata polyfill. For more information on decorators, check out this article (in Russian).

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

Passing data through Angular2 router: a comprehensive guide

I am currently developing a web application with the latest version of Angular (Angular v2.0.0). In my app, I have a sub-navigation and I want to pass data to a sub-page that loads its own component through the router-outlet. According to Angular 2 docume ...

Inquiring about Vue 3 with TypeScript and Enhancing Types for Compatibility with Plugins

I've been struggling to find a working example of how to implement type augmentation with Vue3 and TypeScript. I have searched for hours without success, trying to adapt the Vue2 documentation for Vue3. It appears that the Vue object in the vue-class ...

Permitted the usage of a global variable of any type as the return value of a function that does not explicitly define its

Here's a snippet of TypeScript code that compiles successfully: let testVar: any; const testFunc: () => number = () => { return testVar; }; Why does this code compile without errors? What is the reasoning behind it? ...

When attempting to register a custom Gamepad class using GamepadEvent, the conversion of the value to 'Gamepad' has failed

I have been working on developing a virtual controller in the form of a Gamepad class and registering it. Currently, my implementation is essentially a duplicate of the existing Gamepad class: class CustomController { readonly axes: ReadonlyArray<nu ...

Typescript typings for child model/collection structures

I have encountered an issue while trying to implement a Model/Collection pattern with various typings. Both the Model and Collection classes have a method called serialize(). When this method is called on the Collection, it serializes all the Model(s) with ...

Manipulating datetime format within an input element of type date using Angular's ngModel

I have a date input in my form that is populated from the controller with a string value for 'dateOfDiagnosis'. The format of this string includes the time as well, like this: "2010-09-08T00:00:00" To bind this value to an input field in Angu ...

What is the reason behind the lag caused by setTimeout() in my application, while RxJS timer().subscribe(...) does not have the same

I am currently working on a component that "lazy loads" some comments every 100ms. However, I noticed that when I use setTimeout for this task, the performance of my application suffers significantly. Here is a snippet from the component: <div *ngFor ...

Ways to display or conceal information depending on the dropdown choice

In my Angular project, I am dealing with a dropdown menu that is followed by some data displayed in a div element. component.html <select class="form-control" id="power" required> <option value="" disabled selected ...

What could be causing the error message (No overload matches this call) to pop up when attempting to subscribe to .valueChanges() in order to retrieve data from Firestore?

Currently, I am developing an Angular application that utilizes Firebase Firestore database through the angularfire2 library. However, I am encountering a challenge. I must admit that my background is more in Java than TypeScript, so there might be some g ...

Typical approach to receiving a transformed object from an HTTP service

One of the services I provide includes a method with the following implementation: public fetchCrawls(page: number): Observable<ICrawl[]>{ return this._http.get(this._crawlsURL + page) .map((res: Response) => { ...

The Heart of the Publisher-Subscriber Design Paradigm

After reading various online articles on the Publisher-Subscriber pattern, I have found that many include unnecessary domain-specific components or unreliable information inconsistent with OOP standards. I am seeking the most basic and abstract explanatio ...

Is there a way for me to deduce types dynamically?

Is there a way to dynamically infer types, similar to a union type? I am trying to register multiple elements from different parts of the code using a method like registerElement(...), but I am struggling with inferring these new types in TypeScript. This ...

What could be the cause of this malfunction in the Angular Service?

After creating an Angular app with a controller, I noticed that while I can successfully interact with the controller using Postman (as shown in the screenshot below), I faced issues with displaying data at the frontend. I implemented a new component alon ...

NG build error: Module parsing failed due to an unexpected token - no updates made

Two days ago, out of nowhere, we started encountering build errors during deployment using GitLab CI. No alterations have been made to the build scripts, and none of the versions of NPM, NG, or Angular have been modified. The same compilation commands cont ...

The width of the Bootstrap tooltip changes when hovered over

Currently, I am facing a peculiar issue with my angular web-application. Within the application, there is a matrix displaying data. When I hover over the data in this matrix, some basic information about the object pops up. Strangely, every time I hover ov ...

Exploration of mapping in Angular using the HttpClient's post

After much consideration, I decided to update some outdated Angular Http code to use HttpClient. The app used to rely on Promise-based code, which has now been mostly removed. Here's a snippet of my old Promise function: public getUser(profileId: nu ...

What causes Gun.js to generate duplicate messages within a ReactJs environment?

I need assistance with my React application where gun.js is implemented. The issue I am facing is that messages are being duplicated on every render and update. Can someone please review my code and help me figure out what's wrong? Here is the code s ...

checkbox with an option tag

I need help with implementing multi-select checkboxes inside an Angular 4 application. The checkboxes are not appearing next to the team names as intended. Can anyone assist me with this issue? Below is a snippet of my HTML code: <select class="form-c ...

Encountering an issue during the registration of reducers with ActionReducerMap: "does not match the type 'ActionReducerMap<AppState, Action>'"

Here is a simplified version of my Angular 5 application. I have a reducer that needs to be registered in the root module. The problem arises in LINE A where I encounter the following error: ERROR in src/app/store/app.reducers.ts(7,14): error TS2322: Type ...

Joi has decided against incorporating custom operators into their extended features

I am having trouble extending the joi class with custom operators. My goal is to validate MongoDB Ids, but when I try to use the extended object, I encounter the following error: error: uncaughtException: JoiObj.string(...).objectId is not a function TypeE ...