benjaminz
benjaminz

Reputation: 3228

Use compiler API for type inference

I'm trying to use TypeScript's compiler API to perform very basic type inference, but I couldn't find anything helpful from the documentation or google search.

Essentially, I want to have a function inferType that takes a variable and return its inferred definition:

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"

Is there anyway I can achieve this through the compiler API? If not, is there anyway I can achieve this any other way?

Upvotes: 8

Views: 3572

Answers (4)

cancerbero
cancerbero

Reputation: 7037

You can play with my TypeScript Compiler API Playground example of LanguageService type checking example: https://typescript-api-playground.glitch.me/#example=ts-type-checking-source

Also this is node.js script that parses input typescript code and it infer the type of any symbol according on how it's used. It uses TypeScript Compiler API , creates a Program, and then the magic is just "program.getTypeChecker().getTypeAtLocation(someNode)"

Working example: https://github.com/cancerberoSgx/typescript-plugins-of-mine/blob/master/typescript-ast-util/spec/inferTypeSpec.ts

If you are not familiar with Compiler API start here. Also you have a couple of projects that could make it easier:

good luck

Upvotes: 6

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249466

Option 1

You can use the compiler API to achieve this by using an emit transformer. The emit transformer receives the AST during the emit process and it can modify it. Transformers are used internally by the compiler to transform the TS AST into a JS AST. The resulting AST is then written to file.

What we will do is create a transformer that, when it encounters a function named inferType it will add an extra argument to the call that will be the typescript type name.

transformation.ts

import * as ts from 'typescript'
// 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 {
            // If we have a call expression
            if (ts.isCallExpression(node)) {
                let target = node.expression;
                // that calls inferType
                if(ts.isIdentifier(target) && target.escapedText == 'inferType'){
                    // We get the type of the argument
                    var type = typeChecker.getTypeAtLocation(node.arguments[0]);
                    // And then we get the name of the type
                    var typeName = typeChecker.typeToString(type)
                    // And we update the original call expression to add an extra parameter to the function
                    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);
}
// Compile a file
var cmd = ts.parseCommandLine(['test.ts']);
// Create the program
let program = ts.createProgram(cmd.fileNames, cmd.options);

//Emit the program with our extra 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"

result file test.js

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 is just a proof of concept, you would need to further test. Also integrating this into your build process might be non trivial, basically you would need to replace the original compiler with this custom version that does this custom transform

Option 2

Another option would be to use the compiler API to perform a transformation of the source code before compilation. The transformation would insert the type name into the source file. The disadvantage is that you would see the type parameter as a string in the source file, but if you include this transformation in your build process it will get updated automatically. The advantage is that you can use the original compiler and tools without changing anything.

transformation.ts

import * as ts from 'typescript'

function transformFile(program: ts.Program, file: ts.SourceFile): ts.SourceFile {
    let empty = ()=> {};
    // Dummy 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 {
        // If we have a call expression
        if (ts.isCallExpression(node)) {
            let target = node.expression;
            // that calls inferType
            if(ts.isIdentifier(target) && target.escapedText == 'inferType'){
                // We get the type of the argument
                var type = typeChecker.getTypeAtLocation(node.arguments[0]);
                // And then we get the name of the type
                var typeName = typeChecker.typeToString(type)
                // And we update the original call expression to add an extra parameter to the function
                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;
}

// Compile a file
var cmd = ts.parseCommandLine(['test.ts']);
// Create 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: "" };
// The type name parameter is added/updated automatically when you run the code above.
inferType(bar, "number[]");
inferType(bar2, "number"); 
inferType(foo, "(a: number[], b: number) => number"); 
inferType(f, "{ test: string; }");

Upvotes: 2

Pavel
Pavel

Reputation: 2554

Another way to use decorators.

function inspectType(target: Object, propKey: string): any {
}

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

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

Note, to make it work its needed enable options in config:

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

And use reflect-metadata polyfil. More details on decorators in my article (rus).

Upvotes: 0

basarat
basarat

Reputation: 276171

Is there anyway I can achieve this through the compiler API

The compiler API can let you inspect code when code is a string. e.g.

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 infer things about the code

If not, is there anyway I can achieve this any other way?

Use typeof although its significantly limited.

Alternatively load self code using nodejs __filename (will only work in node and only if running thought ts-node i.e. raw TS ): https://nodejs.org/api/globals.html#globals_filename.

Upvotes: 0

Related Questions