Pure TypeScript Setup for Simple Projects

Posted on .

I've long had certain issues with modern web development, and I even wrote a little tool of my own to help me manage such an environment. So I look for ways to minimise the complexity of my setups while still maintaining some modern conveniences. Now I've started to use a setup that relies on TypeScript and modern browsers' builtin features. This is a very minimal setup consisting just of TypeScript, plain CSS, and maybe a tiny build script.

How it works

The TypeScript compiler tsc can be used to generate JavaScript modules. Those modules can be loaded in modern browsers with the <script type="module"> tag. By combining these facts, you can actually write frontend projects that do not contain a local NPM installation at all. tsc itself you need to install with NPM, but that can be done globally.

To see how it works, you can inspect the files of this blog as an example. The source TS of the index file shows how it looks in a nutshell. You can import other files as normal and the browser will load them as needed. You can use async code and tsc will compile in the necessary helpers, or you can set "target": "es2017" in tsconfig.json to use browsers' native async/await support. If you need to import files dynamically, you can use the import() function.

This makes for a really streamlined experience. No NPM, no webpack configuration, and no fiddling with 0.x.y packages. You just run tsc and it does the compiling, and even generates source maps for you. When dealing with assets and extra files, I might augment it with a small build script:

#!/usr/bin/env bash

set -eux
set -o pipefail


rm -rf ${TARGETDIR}
mkdir -p ${TARGETDIR}
cp -r assets ${TARGETDIR}
cp -r build ${TARGETDIR}
cp -r vendor ${TARGETDIR}/build

Run ./build.sh and you're all set!

Caveat emptor

This way has worked pretty well for small projects that I've done, such as this blog's frontend. That said, it has some big flipping caveats, so read on.

Only for modern browsers

Internet Explorer doesn't support ES modules. tsc also doesn't do polyfilling for you. Don't do this if you need to support old browsers.

No NPM means no NPM

If you don't have NPM, you have to think about how to handle dependencies. For this blog, I put them in a vendor folder in true retro style. This means there is no tooling to update it. If you have many dependencies, this will become a hassle fast. So don't do it. Though personally I feel usually most of those dependencies are not needed.


tsc does not rewrite import statements when compiling to ES modules. But browsers won't understand importing without an extension, so you need to write imports as import { foo } from "./bar.js";. TS will understand this and work properly, but it looks unfortunate.


With CSS custom properties AKA CSS variables, writing plain CSS for a simple project has become much more bearable. You can take a look at this blog's variables for inspiration. But there are still issues. CSS variables cannot be used in media queries, so you will either have to repeat your breakpoint values in every file or stick to simpler styles that you can implement using variables like this:

:root {
  --layout-padding: 20px;

@media(min-width: 1000px) {
  :root {
    --layout-padding: 40px;

Another thing that I miss are SCSS's very useful lighten/darken functions for colors. You could use SCSS by installing a processing tool for it locally (lose no-NPM property) or globally (more global NPM pollution) and calling it in your build script. Of course, if you start adding tools like this, you will have to be careful that you don't end up replicating a normal frontend build system!

Request chains

Any top-level imports in your TS and @import statements in CSS will result in new HTTP requests. These requests will delay loading of your scripts/styles. If you have deep import chains, the request latencies start to add up. HTTP/2 helps with this (more concurrent requests). You can also use the <link type="preload"> tag to hint to the browser that these files should be loaded. But best to just keep the chains short or use dynamic loading, like I do here with Disqus and code highlighting.

How far can it go?

This is a setup for small projects. Chances are that when a project grows bigger, it starts to need the tools that a normal frontend build system provides. The aim for this setup was for me to see how low friction I could make starting a new small personal project, and so far it has succeeded. I've enjoyed using it a lot and will try it out on new things until I hit a breaking point. For example I would welcome any suggestions on how to overcome the mentioned CSS issues.

One thing I'm interested to try out is to write a DOM library to manage the pain points of rendering elements in TS. I already have a little beginning in one of my projects and that does probably 3/4 of what I want already. Just add list and table handling, taking inspiration from RE:DOM, and that would cover my use cases nicely. But that would be a blog post for another time. :)