Russley

Blog

Running TypeScript on Websites

Using TypeScript on the frontend.
TypeScript
Created 29 days ago
Updated 30 days ago

Contents

Motivation

The TypeScript playground is a great tool for quickly testing out code snippets. But how does it work? Can we create something similar for our own sites.

We can!

TypeScript

const x = 1;

console.log(x);

x = "whooops";
Compiling...

This is an example of a TypeScript snippet that we can compile in the browser and get the errors back. In the case above, the typescript checking can be enabled by adding // tscheck at the top of the file. Clicking the Show All button will display the hidden tooling comments.

Starting With Basics

First we need to start some basic usage of the TypeScript compiler via its API. We can simplify some sample code on the page, Using the Compiler API, to get started. This means we can run our mini-compiler and see that we got an error. Generally, we need to:

  1. create a program (setup)
  2. then emit it (compilation)
  3. and finally get the diagnostics (errors).
TypeScript

import * as ts from "typescript";

const options: ts.CompilerOptions = {
  // ...
};

function compile(fileNames: string[]): void {
  let program = ts.createProgram(fileNames, options);
  let emitResult = program.emit();

  let allDiagnostics = ts
    .getPreEmitDiagnostics(program)
    .concat(emitResult.diagnostics);

  for (const diagnostic of allDiagnostics) {
    // We got an error, log it!!
  }
}

compile(["index.ts"]);

'Files' on the Web

A major difference between the TypeScript compiler tsc and the TypeScript playground is tsc reads files from our filesystem, whereas the playground is just supplied a string of our code. Fortunately the TS devs thought of this and created a way for us to supply 'virtual' files to the compiler. This library is called @typescript/vfs. It performs two important actions for us: it creates a virtual file system (our code strings) and a virtual compiler host (our libs).

Using the TypeScript CDN

There are other ways to setup the default libraries for TypeScript, but the easiest here is to use the CDN. TS VFS provides us createDefaultMapFromCDN to do this.

TypeScript

import { createDefaultMapFromCDN } from "@typescript/vfs";

const fsMap = await createDefaultMapFromCDN(
  compilerOptions,
  ts.version,
  true,
  ts
);

The resulting fsMap is our virtual file system; this allows us to begin to add our own code to it.

TypeScript

fsMap.set("index.ts", ourCode);

Creating a Compiler Host

TypeScript

const system = tsvfs.createSystem(fsMap);
const host = tsvfs.createVirtualCompilerHost(system, compilerOptions, ts);

Similar to our example without the virtual file system, we can now create a program by supplying it the host we just created as well as the files we wish to compile.

TypeScript

const program = ts.createProgram({
  rootNames: ["index.ts"], // our file
  options: compilerOptions,
  host: host.compilerHost,
});

Note: many examples online will provide rootNames as [...fsMap.keys()], but doing this will likely cause many errors as we're not looking to compile the TypeScript libraries, which are included in our virtual file system.

Getting diagnostics

Similar to before, we can get the diagnostics from the program.

TypeScript

const allDiagnostics = ts
  .getPreEmitDiagnostics(program)
  .concat(emitResult.diagnostics);

return allDiagnostics.map((d) => {
  if (d.file) {
    const { line, character } = ts.getLineAndCharacterOfPosition(
      d.file,
      d.start!
    );
    const message = ts.flattenDiagnosticMessageText(d.messageText, "\n");
    return `${d.file.fileName} (${line + 1},${character + 1}): ${message}`;
  }
  return ts.flattenDiagnosticMessageText(d.messageText, "\n");
});

Final Code

TypeScript

import {
  createDefaultMapFromCDN,
  createSystem,
  createVirtualCompilerHost,
} from "@typescript/vfs";
import ts from "typescript";
import lzstring from "lz-string";

const compilerOptions: ts.CompilerOptions = {
  strict: true,
};

export async function getTypeScriptErrors(code: string, filename?: string) {
  const myFilename = filename ?? "index.ts";
  const fsMap = await createDefaultMapFromCDN(
    compilerOptions,
    ts.version,
    true,
    ts,
    lzstring
  );
  fsMap.set(myFilename, code);

  const system = createSystem(fsMap);
  const host = createVirtualCompilerHost(system, compilerOptions, ts);

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

  const emitResult = program.emit();
  const allDiagnostics = ts
    .getPreEmitDiagnostics(program)
    .concat(emitResult.diagnostics);

  return allDiagnostics.map((diagnostic) => {
    if (diagnostic.file) {
      const { line, character } = ts.getLineAndCharacterOfPosition(
        diagnostic.file,
        // biome-ignore lint/style/noNonNullAssertion: <explanation>
        diagnostic.start!
      );
      const message = ts.flattenDiagnosticMessageText(
        diagnostic.messageText,
        "\n"
      );
      return `${diagnostic.file.fileName} (${line + 1},${
        character + 1
      }): ${message}`;
    }
    return ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
  });
}

Conclusion

There are many use-cases for being able to compile TypeScript in a user's browser. Some additional ideas could include: