What is the process for performing type checking on a block of TypeScript code stored in memory?

I am currently integrating TypeScript support into my project called Data-Forge Notebook.

My goal is to compile, perform type checking, and evaluate snippets of TypeScript code within the application.

Compiling the code seems straightforward using the transpileModule function. Here's a snippet demonstrating how I convert TypeScript code to JavaScript for evaluation:

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, I encounter an issue when attempting to compile TypeScript code with errors present.

For instance, the following function contains a type error but is still transpiled without any error notifications:

function foo(): string {
    return 5;
}

While transpiling is useful, I also want to be able to provide error feedback to users.

My question is how can I achieve this while also performing type checking and generating error messages for semantic issues?

Please note that I prefer not to save the TypeScript code to a file as it would impact performance. My aim is to compile and check code snippets stored in memory.

Answer №1

Scenario 1 - Utilizing Memory Only without File System Access (e.g. on the web)

Completing this task is not simple and may require some time to accomplish. While there might be a more straightforward approach, I have yet to discover one.

  1. Create a ts.CompilerHost with methods like fileExists, readFile, directoryExists, getDirectories(), etc., that retrieve data from memory instead of the actual file system.
  2. Import the relevant lib files into your in-memory file system based on your requirements (e.g., lib.es6.d.ts or lib.dom.d.ts).
  3. Add your in-memory file to the in-memory file system as well.
  4. Develop a program (using ts.createProgram) and provide your custom ts.CompilerHost.
  5. Use
    ts.getPreEmitDiagnostics(program)
    to obtain the diagnostics.

Imperfect Sample

Below is a brief imperfect example that lacks proper implementation of an in-memory file system and fails to load the lib files (resulting in global diagnostic errors... which can be ignored or addressed by calling specific methods on program other than program.getGlobalDiagnostics(). For information about the behavior of ts.getPreEmitDiagnostics, refer to the link here):

import * as ts from "typescript";

console.log(getDiagnosticsForText("const t: number = '';").map(d => d.messageText));

function getDiagnosticsForText(text: string) {
    const dummyFilePath = "/file.ts";
    const textAst = ts.createSourceFile(dummyFilePath, text, ts.ScriptTarget.Latest);
    const options: ts.CompilerOptions = {};
    const host: ts.CompilerHost = {
        fileExists: filePath => filePath === dummyFilePath,
        directoryExists: dirPath => dirPath === "/",
        getCurrentDirectory: () => "/",
        getDirectories: () => [],
        getCanonicalFileName: fileName => fileName,
        getNewLine: () => "\n",
        getDefaultLibFileName: () => "",
        getSourceFile: filePath => filePath === dummyFilePath ? textAst : undefined,
        readFile: filePath => filePath === dummyFilePath ? text : undefined,
        useCaseSensitiveFileNames: () => true,
        writeFile: () => {}
    };
    const program = ts.createProgram({
        options,
        rootNames: [dummyFilePath],
        host
    });

    return ts.getPreEmitDiagnostics(program);
}

Scenario 2 - File System Accessibility

If you have access to the file system, the process becomes much simpler, and you can utilize a function similar to the one provided below:

import * as path from "path";

function getDiagnosticsForText(
    rootDir: string,
    text: string,
    options?: ts.CompilerOptions,
    cancellationToken?: ts.CancellationToken
) {
    options = options || ts.getDefaultCompilerOptions();
    const inMemoryFilePath = path.resolve(path.join(rootDir, "__dummy-file.ts"));
    const textAst = ts.createSourceFile(inMemoryFilePath, text, options.target || ts.ScriptTarget.Latest);
    const host = ts.createCompilerHost(options, true);

    overrideIfInMemoryFile("getSourceFile", textAst);
    overrideIfInMemoryFile("readFile", text);
    overrideIfInMemoryFile("fileExists", true);

    const program = ts.createProgram({
        options,
        rootNames: [inMemoryFilePath],
        host
    });

    return ts.getPreEmitDiagnostics(program, textAst, cancellationToken);

    function overrideIfInMemoryFile(methodName: keyof ts.CompilerHost, inMemoryValue: any) {
        const originalMethod = host[methodName] as Function;
        host[methodName] = (...args: unknown[]) => {
            // resolve the path because TypeScript will normalize it
            // to forward slashes on Windows
            const filePath = path.resolve(args[0] as string);
            if (filePath === inMemoryFilePath)
                return inMemoryValue;
            return originalMethod.apply(host, args);
        };
    }
}

// example...
console.log(getDiagnosticsForText(
    __dirname,
    "import * as ts from 'typescript';\n const t: string = ts.createProgram;"
));

By following this method, the compiler will search for a node_modules folder in the provided rootDir and utilize the typings located there (without requiring them to be loaded into memory separately).

Update: Simplified Solution

A library named @ts-morph/bootstrap has been developed to streamline the setup process with the Compiler API. It also automatically loads TypeScript lib files when using an in-memory file system.

import { createProject, ts } from "@ts-morph/bootstrap";

const project = await createProject({ useInMemoryFileSystem: true });

const myClassFile = project.createSourceFile(
    "MyClass.ts",
    "export class MyClass { prop: string; }",
);

const program = project.createProgram();
ts.getPreEmitDiagnostics(program); // verify these

Answer №2

To resolve the issue, I utilized initial guidance from David Sherret and a helpful tip from Fabian Pirklbauer (founder of TypeScript Playground).

My approach involved developing a proxy CompilerHost to envelop an actual CompilerHost. This proxy has the ability to provide the in-memory TypeScript code required for compilation. The genuine CompilerHost can load the necessary default TypeScript libraries ensuring the avoidance of errors associated with built-in TypeScript data types.

Code Snippet

import * as ts from "typescript";

//
// TypeScript code snippet containing semantic/type error.
//
const code 
    = "function foo(input: number) {\n" 
    + "    console.log('Hello!');\n"
    + "};\n" 
    + "foo('x');"
    ;

//
// Compilation result of the TypeScript code.
//
export interface CompilationResult {
    code?: string;
    diagnostics: ts.Diagnostic[]
};

//
// Verification and compilation of in-memory TypeScript code for potential 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("==== Code Evaluation ====");
console.log(code);
console.log();

const libs = [ 'es2015' ];
const result = compileTypeScriptCode(code, libs);

console.log("==== Outputted Code ====");
console.log(result.code);
console.log();

console.log("==== Diagnostics ====");
for (const diagnostic of result.diagnostics) {
    console.log(diagnostic.messageText);
}
console.log();

Final Output

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

Find complete working example on my Github profile.

Answer №3

My goal was to assess a string that represents TypeScript and achieve the following:

  • Have visibility of errors related to types
  • Use import statements for source files and dependencies from node_modules
  • Reuse the TypeScript settings (tsconfig.json, etc) from the current project

To accomplish this, I devised a method involving the creation of a temporary file and executing it with the help of the ts-node utility using child_process.spawn

This process necessitates having ts-node operational in the current shell; you may need to execute:

npm install --global ts-node

or

npm install --save-dev ts-node

The code snippet below demonstrates how to use ts-node to run any piece of TypeScript code:

import path from 'node:path';
import childProcess from 'node:child_process';
import fs from 'node:fs/promises';

let getTypescriptResult = async (tsSourceCode, dirFp=__dirname) => {
    // Create a temporary file to store the TypeScript code for execution
    let tsPath = path.join(dirFp, `${Math.random().toString(36).slice(2)}.ts`);
    await fs.writeFile(tsPath, tsSourceCode);

    try {
        // Execute the ts-node shell command using the temporary file
        let output = [] as Buffer[]; 
        let proc = childProcess.spawn('ts-node', [ tsPath ], { shell: true, cwd: process.cwd() });
        proc.stdout.on('data', d => output.push(d));
        proc.stderr.on('data', d => output.push(d));
        
        return {
          code: await new Promise(r => proc.on('close', r)),
          output: Buffer.concat(output).toString().trim()
        };
    } finally { await fs.rm(tsPath); } // Clean up temporary file
};

Now, by running the following line of code:

let result = await getTypescriptResult('const str: string = 123;');
console.log(result.output);

After a short delay, you will see that result.output contains error information like:

/Users/..../index.ts:859
    return new TSError(diagnosticText, diagnosticCodes, diagnostics);
            ^
TSError: ⨯ Unable to compile TypeScript:
6y4ln36ox8c.ts(2,7): error TS2322: Type 'number' is not assignable to type 'string'.

    at createTSError (/Users/..../index.ts:859:12)
    ...

All pertinent data should be presented here - though some parsing may be necessary!

This technique also supports incorporating import statements:

let typescript = `
import dependency from '@namespace/dependency';
import anotherDependency from './src/source-file';

doStuffWithImports(dependency, anotherDependency);
`;

let result = await getTypescriptResult(typescript, __dirname);
console.log(result.output);

Note that if you place the getTypescriptResult function in a separate file, it's advisable to provide __dirname as the second argument when invoking it, ensuring that module resolution functions relative to the current file rather than the one defining getTypescriptResult.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Assigning enum type variable using string in TypeScript

How can I dynamically assign a value to a TypeScript enum variable? Given: enum options { 'one' = 'one', 'two' = 'two', 'three' = 'three'} let selected = options.one I want to set the variable " ...

A guide to configuring VSCode to recognize the DefinitelyTyped global variable (grecaptcha) within a Vuejs 3 TypeScript project

I have recently set up a new project using Vue.js 3 and TypeScript by running the command npm init vue@latest. I now want to integrate reCaptcha v2 into the project from scratch, without relying on pre-existing libraries like vue3-recaptcha-v2. Instead of ...

CDK Error: Unable to locate MethodResponse in AWS API Gateway configuration

I'm facing an issue in vscode while trying to access the MethodResponse interface from apigateway. Unfortunately, I'm getting an error message: The type 'typeof import(".../node_modules/aws-cdk-lib/aws-apigateway/index")' d ...

Exploring Angular 4: Embracing the Power of Observables

I am currently working on a project that involves loading and selecting clients (not users, but more like customers). However, I have encountered an issue where I am unable to subscribe to the Observables being loaded in my component. Despite trying vario ...

Unable to connect to web3 object using typescript and ethereum

Embarking on a fresh project with Angular 2 and TypeScript, I kicked things off by using the command: ng new myProject Next, I integrated web3 (for Ethereum) into the project through: npm install web3 To ensure proper integration, I included the follow ...

Guide to setting up a trigger/alert to activate every 5 minutes using Angular

limitExceed(params: any) { params.forEach((data: any) => { if (data.humidity === 100) { this.createNotification('warning', data.sensor, false); } else if (data.humidity >= 67 && data.humidity <= 99.99) { ...

The browser is throwing errors because TypeScript is attempting to convert imports to requires during compilation

A dilemma I encountered: <script src="./Snake.js" type="text/javascript"></script> was added to my HTML file. I have a file named Snake.ts which I am compiling to JS using the below configuration: {target: "es6", module: "commonjs"} Howeve ...

Errors related to missing RxJS operators are occurring in the browser, but are not showing up in Visual Studio

Recently, I encountered a peculiar problem with my Angular4 project, which is managed under Angular-CLI and utilizes the RxJS library. Upon updating the RxJS library to version 5.5.2, the project started experiencing issues with Observable operators. The s ...

React and Typescript Multimap Approach

I'm a beginner in TypeScript and I am struggling to understand how to create a multimap. The code I have is shown below. My goal is to loop through the itemArray and organize the items based on their date values. I want to use the date as the key for ...

Is it possible to retrieve the original array after removing filters in Angular?

Within my component, I have an array that I am filtering based on a search string. The filtering works as expected when the user inputs characters into the search field. However, I am encountering an issue when attempting to display all records again after ...

Mastering the art of bi-directional data binding with nested arrays in Angular

Imagine you have a to-do list with various tasks, each containing multiple subtasks. You want the ability to change the subtask data, but why is Angular not properly two-way binding the data for the subtasks? HTML <div *ngFor="let task of tasks"> ...

Filtering data in Angular from an HTML source

Looking to filter a simple list I have. For example: <div *ngFor="let x of data"></div> Example data: data = [ { "img" : "assets/img/photos/05.jpg", "title" : "Denim Jeans", "overview": "today" ...

Break free/Reenter a function within another function

Is there a way to handle validation errors in multiple task functions using TypeScript or JavaScript, and escape the main function if an error occurs? I am working in a node environment. const validate = () => { // Perform validation checks... // ...

Arrange the array based on the order of the enumeration rather than its values

Looking to create an Array of objects with enum properties. export enum MyEnum { FIXTERM1W = 'FIXTERM_1W', FIXTERM2W = 'FIXTERM_2W', FIXTERM1M = 'FIXTERM_1M', FIXTERM2M = 'FIXTERM_2M', FIXTERM3M = 'FIX ...

Can you guide me on how to programmatically set an option in an Angular 5 Material Select dropdown (mat-select) using typescript code?

I am currently working on implementing an Angular 5 Material Data Table with two filter options. One filter is a text input, while the other is a dropdown selection to filter based on a specific field value. The dropdown is implemented using a "mat-select" ...

The service has terminated unexpectedly because of signal: Ended prematurely: 9

I'm encountering the error 'Service exited due to signal: Killed: 9' and am unable to launch my app. I've come across information suggesting that this may be caused by memory leaks or a lengthy startup time for the app. In all honesty, ...

Tips for patiently waiting for a method to be executed

I have encountered a situation where I need to ensure that the result of two methods is awaited before proceeding with the rest of the code execution. I attempted to use the async keyword before the function name and await before the GetNavigationData() me ...

When using Vue with Vuetify, be aware that object literals can only specify known properties. In this case, the type 'vuetify' does not exist in the ComponentOptions of Vue with DefaultData

Started a fresh Vue project with TypeScript by following this guide: https://v2.vuejs.org/v2/guide/typescript.html If you don't have it installed yet, install Vue CLI using: npm install --global @vue/cli Create a new project and choose the "Manual ...

Creating an Inner Join Query Using TypeORM's QueryBuilder: A Step-by-Step Guide

Hello there! I'm new to TypeORM and haven't had much experience with ORM. I'm finding it a bit challenging to grasp the documentation and examples available online. My main goal is to utilize the TypeORM QueryBuilder in order to create this ...

Error: TypeScript compilation failed due to absence of tsc command in the system

Hello, I recently installed TypeScript and encountered an issue when trying to initialize tsc -v in the terminal. The error message I received was "bash: tsc: command not found." During the installation process, I used npm install -g typescript@latest whi ...