Search code examples
typescripttypecheckingtranspilertypescript-compiler-api

How do I type check a snippet of TypeScript code in memory?


I'm implementing TypeScript support into my application Data-Forge Notebook.

I need to compile, type check and evaluate snippets of TypeScript code.

Compilation appears to be no problem, I'm using transpileModule as shown below to convert a snippet of TS code into JavaScript code that can be evaluated:

import { transpileModule, TranspileOptions } from "typescript";

const transpileOptions: TranspileOptions = {
    compilerOptions: {},
    reportDiagnostics: true,
};

const tsCodeSnippet = " /* TS code goes here */ ";
const jsOutput = transpileModule(tsCodeSnippet, transpileOptions);
console.log(JSON.stringify(jsOutput, null, 4));

However there is a problem when I try an compile TS code that has an error.

For example the following function has a type error, yet it is transpiled without any error diagnostics:

function foo(): string {
    return 5;
}

Transpiling is great, but I'd also like to be able to display errors to my user.

So my question is how can do this but also do type checking and produce errors for semantic errors?

Note that I don't want to have to save the TypeScript code to a file. That would be an unecessary performance burden for my application. I only want to compile and type check snippets of code that are held in memory.


Solution

  • I've solved this problem building on some original help from David Sherret and then a tip from Fabian Pirklbauer (creator of TypeScript Playground).

    I've created a proxy CompilerHost to wrap a real CompilerHost. The proxy is capable of returning the in-memory TypeScript code for compilation. The underlying real CompilerHost is capable of loading the default TypeScript libraries. The libraries are needed otherwise you get loads of errors relating to built-in TypeScript data types.

    Code

    import * as ts from "typescript";
    
    //
    // A snippet of TypeScript code that has a semantic/type error in it.
    //
    const code 
        = "function foo(input: number) {\n" 
        + "    console.log('Hello!');\n"
        + "};\n" 
        + "foo('x');"
        ;
    
    //
    // Result of compiling TypeScript code.
    //
    export interface CompilationResult {
        code?: string;
        diagnostics: ts.Diagnostic[]
    };
    
    //
    // Check and compile in-memory TypeScript code for errors.
    //
    function compileTypeScriptCode(code: string, libs: string[]): CompilationResult {
        const options = ts.getDefaultCompilerOptions();
        const realHost = ts.createCompilerHost(options, true);
    
        const dummyFilePath = "/in-memory-file.ts";
        const dummySourceFile = ts.createSourceFile(dummyFilePath, code, ts.ScriptTarget.Latest);
        let outputCode: string | undefined = undefined;
    
        const host: ts.CompilerHost = {
            fileExists: filePath => filePath === dummyFilePath || realHost.fileExists(filePath),
            directoryExists: realHost.directoryExists && realHost.directoryExists.bind(realHost),
            getCurrentDirectory: realHost.getCurrentDirectory.bind(realHost),
            getDirectories: realHost.getDirectories.bind(realHost),
            getCanonicalFileName: fileName => realHost.getCanonicalFileName(fileName),
            getNewLine: realHost.getNewLine.bind(realHost),
            getDefaultLibFileName: realHost.getDefaultLibFileName.bind(realHost),
            getSourceFile: (fileName, languageVersion, onError, shouldCreateNewSourceFile) => fileName === dummyFilePath 
                ? dummySourceFile 
                : realHost.getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile),
            readFile: filePath => filePath === dummyFilePath 
                ? code 
                : realHost.readFile(filePath),
            useCaseSensitiveFileNames: () => realHost.useCaseSensitiveFileNames(),
            writeFile: (fileName, data) => outputCode = data,
        };
    
        const rootNames = libs.map(lib => require.resolve(`typescript/lib/lib.${lib}.d.ts`));
        const program = ts.createProgram(rootNames.concat([dummyFilePath]), options, host);
        const emitResult = program.emit();
        const diagnostics = ts.getPreEmitDiagnostics(program);
        return {
            code: outputCode,
            diagnostics: emitResult.diagnostics.concat(diagnostics)
        };
    }
    
    console.log("==== Evaluating code ====");
    console.log(code);
    console.log();
    
    const libs = [ 'es2015' ];
    const result = compileTypeScriptCode(code, libs);
    
    console.log("==== Output code ====");
    console.log(result.code);
    console.log();
    
    console.log("==== Diagnostics ====");
    for (const diagnostic of result.diagnostics) {
        console.log(diagnostic.messageText);
    }
    console.log();
    

    Output

    ==== Evaluating code ====
    function foo(input: number) {
        console.log('Hello!');
    };
    foo('x');
    =========================
    Diagnosics:
    Argument of type '"x"' is not assignable to parameter of type 'number'.
    

    Full working example available on my Github.