How do I compile TypeScript for the web?

tl;dr use Vite. Example.

You may be wondering if the TypeScript compiler is able to generate JavaScript code that can be used on a website. The answer is somewhat ambiguous, but in most cases, the answer is no.

Simple

For a basic example of how this works, you can see an example here. First, set outDir in tsconfig. Now when I run tsc, it generates JavaScript files that I can then import into my index.html file. This seems to work without any issues.

NPM packages

The problem comes when you try to use an npm package. tsc can't bundle an npm package. tsc is not a bundler, it just compiles your TypeScript files to JavaScript. In this example, I'm importing an npm package. When I run tsc, the output still contains my import line, but TypeScript didn't even try to bundle the contents of tiny-package for me.

However, Vite can easily (easily!) solve this for you. See this example. Seriously, you won't believe how easy it is. All you need to do is

  1. Install vite (npm i -D vite), and
  2. Instead of starting a Python server, start vite (npx vite).

You're still building with tsc, but Vite is serving your files and it can handle npm packages.

Single file

In the simple example above, index.html includes index.js. index.js imports foo.js. This import happens using ESM (ECMAScript module) syntax. This works fine since browsers now support this natively.

Here, I'm outputting multiple files. My TypeScript files get generated into corresponding JavaScript files:

index.ts -> index.js
foo.ts -> foo.js

I can output a single file instead using outFile, as in this example. I need to set "module": "AMD" because I can't output a single file using ESM syntax.

AMD modules are the kind that use the require and define syntax:

define("foo", ["require", "exports"], function (require, exports) {
  "use strict";
  // etc
});

But! If you now include the generated file in index.html, you'll see an error:

ReferenceError: Can't find variable: define

This is because AMD syntax is not natively supported in browsers. You'll need to include require.js first to make this work. The easiest way is to add the unpkg link:

<script src="https://unpkg.com/requirejs/require.js"></script>

You'll also need to explicitly require the index module to see the console.logs appear:

<script>
    // or requirejs
    require(["index"], function (index) {
        console.log(index);
    });
</script>

Webpack

Webpack fixes all these issues. Webpack takes all your files and combines them into a single file. It handles npm packages too.

Example.

Webpack can get complex, but this example is simple.

First install webpack:

npm install webpack webpack-cli --save-dev

Create a webpack.config.js file:

const path = require("path");

module.exports = {
  mode: "development",
  entry: "./index.ts",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
};

Run webpack (add this to scripts in package.json):

"build": "webpack"

Tada!

Vite

We already saw how vite works (here's the example again). If, instead of serving files, you want vite to make a build that you can then upload to Vercel or render.com or whatever, run npx vite build. It will look for an index.html file by default, use that as an entry point, and go through all the JavaScript files linked from there.

One change we'll need to make is, we've been linking to the generated JavaScript file from index.html, but now we link straight to the TypeScript file instead.

I'm adding this example for the sake of completeness, but the only change I've made is which js file I'm linking to in index.html.

Vite example.

Vite vs Webpack

Like Webpack, Vite bundles several different JavaScript files for you, handles npm packages, etc. The difference is Webpack was written before ECMAScript modules became a standard, and so it bundles all the files together. Vite tends to take advantage of the fact that you can import ECMAScript modules in the browser.

References