Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 520
benjaminz Avatar asked Mar 19 '18 03:03

benjaminz


People also ask

Does JavaScript use type inference?

An experimental evaluation showed that the inference is powerful, handling the aforementioned benchmarks with no manual type annotation, and that the inferred types enable effective static compilation. JavaScript is one of the most popular programming languages currently in use [6].

What is TypeScript API?

Typescript is a superset of JavaScript with additional features such as static types checking. Typescript is gaining a lot of popularity among JavaScript developers. It is a fast-developing programming language for building extensive applications.

How do you type infer TypeScript?

Using infer in TypeScript It does that by first checking whether your type argument ( T ) is a function, and in the process of checking, the return type is made into a variable, infer R , and returned if the check succeeds: type ReturnType<T> = T extends (... args: any[]) => infer R ?


2 Answers

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:

  • https://dsherret.github.io/ts-simple-ast/
  • https://github.com/RyanCavanaugh/dts-dom

good luck

like image 189
cancerbero Avatar answered Nov 13 '22 05:11

cancerbero


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; }");
like image 43
Titian Cernicova-Dragomir Avatar answered Nov 13 '22 07:11

Titian Cernicova-Dragomir