Reputation: 3228
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
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
Reputation: 249466
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
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
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
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