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
TARGETDIR=../../../priv/static
tsc
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.
.js
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.
CSS
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. :)