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