使用编译器API进行类型推断

时间:2018-03-19 03:10:37

标签: typescript typescript-compiler-api

我正在尝试使用TypeScript的编译器API来执行非常基本的类型推断,但我找不到任何有用的文档或Google搜索。

基本上,我希望有一个函数inferType,它接受​​一个变量并返回其推断的定义:

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"

无论如何我可以通过编译器API实现这一点吗?如果没有,无论如何我还能以其他方式实现这一目标吗?

4 个答案:

答案 0 :(得分:3)

您可以使用我的TypeScript Compiler API Playground语言服务类型检查示例示例:https://typescript-api-playground.glitch.me/#example=ts-type-checking-source

这也是node.js脚本,它解析输入的打字稿代码,并根据它的使用方式推断出任何符号的类型。它使用TypeScript Compiler API,创建一个程序,然后魔术只是" program.getTypeChecker()。getTypeAtLocation(someNode)"

工作示例:https://github.com/cancerberoSgx/typescript-plugins-of-mine/blob/master/typescript-ast-util/spec/inferTypeSpec.ts

如果您不熟悉Compiler API,请从此处开始。此外,您还有一些项目可以让它变得更容易:

祝你好运

答案 1 :(得分:1)

选项1

您可以使用编译器API通过使用emit变换器来实现此目的。发射变压器在发射过程中接收AST并且可以对其进行修改。编译器在内部使用变换器将TS AST转换为JS AST。然后将生成的AST写入文件。

我们要做的是创建一个变换器,当它遇到一个名为inferType的函数时,它会为调用添加一个额外的参数,这个参数将是typescript类型的名称。

<强> 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"

结果文件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"

注意 这只是一个概念证明,您需要进一步测试。将此集成到您的构建过程中可能并非易事,基本上您需要将此原始编译器替换为执行此自定义转换的自定义版本

选项2

另一种选择是使用编译器API在编译之前执行源代码的转换。转换会将类型名称插入源文件中。缺点是您会在源文件中将type参数视为字符串,但如果在构建过程中包含此转换,它将自动更新。优点是您可以使用原始编译器和工具而无需更改任何内容。

<强> 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; }");

答案 2 :(得分:0)

  

无论如何,我可以通过编译器API实现这一目标

代码是字符串时,编译器API可以让您检查代码。例如

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
  

如果没有,无论如何我还能以其他方式实现这一目标吗?

使用typeof,但significantly limited

或者使用nodejs __filename加载 self 代码(仅在节点中工作且仅在运行思想ts-node即原始TS时):https://nodejs.org/api/globals.html#globals_filename

答案 3 :(得分:0)

使用装饰器的另一种方法。

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

注意,要使它在config中运行所需的启用选项:

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

使用reflect-metadata polyfil。有关my article (rus)中装饰器的更多详细信息。