Converting your vanilla Javascript app to TypeScript
Anvil Engineering
Posted on May 9, 2022
The Javascript language has gone through many updates throughout its long (in internet terms) history. Along with its
rapidly changing ecosystem and maturing developer base came attempts to ease some of Javascript’s shortcomings. Of note,
one of the more significant attempts was CoffeeScript (initial release in 2009) which adds
syntactic sugar and features that make programming easier.
In late 2012, TypeScript was publicly released at version
0.8 1 . Similar to CoffeeScript, TypeScript attempted to
add more features onto Javascript. The biggest feature TypeScript brought to the table, as its name implies, was types.
Types, and all other features that build on top of types such as generics, were already enjoyed by developers on other
languages like Java and C#, but this felt like the start of something big for Javascript.
Fast-forward to 2016 – the first time TypeScript is mentioned
on Stack Overflow’s Developer Survey. In 2016, a whopping 0.47% of
survey respondents have used TypeScript. Skipping ahead two more years
to the 2018 survey and TypeScript jumps to 17.4% of
respondents using TypeScript. You probably get where this is heading now. In
the most recent survey (2021) TypeScript jumped to 30.19%, even moving
past languages like C# and PHP. This is definitely a sign that TypeScript is something to pay attention to and maybe
your vanilla Javascript app could use a little makeover.
This post will go through an example TODO app repo we’ve set up
here: https://github.com/anvilco/anvil-ts-upgrade-example. This repo is in plain vanilla Javascript and runs on Node.js
and uses Express. There are some basic tests included, but it’s as simple as can be. We will go through a few strategies
that one can take when attempting to migrate to using TypeScript from Javascript.
This post will not go through TypeScript programming concepts in depth and will only gloss over them briefly since those
are huge chunks of information themselves. The
official TypeScript Handbook is a great resource if you want
to learn more.
Step 0
Before starting, it’s important to know our starting point with our app’s functionality. We need tests. If you don’t
have any, it’s worth it to at least create a few tests for happy path tests: that is, tests that do the simplest “good”
thing.
Also worth mentioning before you start: have your code on a versioning system (i.e. git). A lot of files are likely to
change and you’ll want an easy way to undo everything.
Install TypeScript
Simple enough. Let’s get started:
$ npm install --save-dev typescript
# or
$ yarn add --dev typescript
TypeScript is a superset of Javascript, so any valid Javascript program is also a valid TypeScript
program 2 . Essentially, if we run our code through the TypeScript
compiler tsc
(which is provided when you install typescript
above) without any major problems, we should be good to
go!
tsconfig.json
After installing, we also need to set up a basic tsconfig.json
file for how we want tsc
to behave with our app. We
will use one of the recommended tsconfig
files
here: https://github.com/tsconfig/bases#centralized-recommendations-for-tsconfig-bases. This contains a list of
community recommended configs depending on your app type. We’re using Node 16 and want to be extremely strict on the
first pass to clean up any bad code habits and enforce some consistency. We’ll use the one
located: https://github.com/tsconfig/bases/blob/main/bases/node16-strictest.combined.json.
{
"display": "Node 16 + Strictest",
"compilerOptions": {
"lib": [
"es2021"
],
"module": "commonjs",
"target": "es2021",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"importsNotUsedAsValues": "error",
"checkJs": true,
// Everything below are custom changes for this app
"allowJs": true,
"outDir": "dist"
},
"include": [
"./src/**/*",
"./__tests__/**/*"
]
}
Compile and fix errors
Now let’s run tsc
:
$ yarn tsc
# or
$ npx tsc
Found 68 errors in 5 files.
Errors Files
13 __tests__/app.test.js:1
28 __tests__/store.test.js:3
14 src/app.js:1
1 src/models/Todo.js:4
12 src/store.js:13
error Command failed with exit code 2.
68 errors. Not too bad especially with extremely strict rules on. Many of the errors are “implicitly has an 'any' type”
and should have easy fixes.
Aside from those, there are a few interesting errors:
src/app.js:1:25 - error TS7016: Could not find a declaration file for module 'express'. 'anvil-ts-upgrade-example/node_modules/express/index.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/express` if it exists or add a new declaration (.d.ts) file containing `declare module 'express';`
1 const express = require('express')
~~~~~~~~~
src/app.js:2:28 - error TS7016: Could not find a declaration file for module 'body-parser'. 'anvil-ts-upgrade-example/node_modules/body-parser/index.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/body-parser` if it exists or add a new declaration (.d.ts) file containing `declare module 'body-parser';`
2 const bodyParser = require('body-parser')
~~~~~~~~~~~~~
These error messages tell us how to fix the errors and also point to how parts of the typing system works in TypeScript.
Packages can provide typing declarations (with the *.d.ts file extension). These are generated automatically through
the tsc
compiler. If you have a publicly accessible TypeScript app, you can also provide official typing declarations
by submitting a pull request to the DefinitelyTyped repo: https://github.com/DefinitelyTyped/DefinitelyTyped.
The two packages in the snippet are very popular modules, so they’ll definitely have type declarations:
# We’re also adding in type declarations for modules used in testing: supertest, jest
npm i --save-dev @types/body-parser @types/express @types/supertest @types/jest
# or
$ yarn add -D @types/body-parser @types/express @types/supertest @types/jest
Let’s check on tsc
:
Found 15 errors in 3 files.
Errors Files
2 src/app.js:13
1 src/models/Todo.js:4
12 src/store.js:13
error Command failed with exit code 2.
From 68 errors to 15. That’s much more manageable. The rest of the errors should now be actually related to our own
code. Let’s take the one that repeats the most:
src/store.js:28:16 - error TS7053: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type '{}'.
28 let item = localStore?.[id]
~~~~~~~~~~~~~~~~
What does this mean? Our localStore
variable is an Object, but its key type is any
. In strict TypeScript, we need to
be clear what our indexes can be. In this case we use numbers as indexes.
Before we fix this, let’s change our file’s extension to the *.ts
: store.js -> store.ts
. This will let tsc
know
this is a TypeScript file, as well as our IDE. Now we start actually writing TypeScript.
interface LocalAppStore {
[key: number]: typeof TodoModel
}
const localStore: LocalAppStore = {}
We’ve created our first interface. It tells the TypeScript compiler what a LocalAppStore
object looks like with its
keys as numbers and its values as a TodoModel
.
We also need to create a new interface TodoData
which defines the object we pass to create and update Todo
instances.
// src/models/Todo.ts (renamed from Todo.js)
export interface TodoData {
id?: number,
title?: string,
description?: string,
}
We can then import that interface and use it as type annotations throughout the app.
Without getting too verbose and explaining all the other smaller errors, you can take a look at the branch we have here
to see what changed: https://github.com/anvilco/anvil-ts-upgrade-example/tree/ts-convert.
In summary, after installing TypeScript and creating its config file we:
Get tsc
to run without failing – not including errors it finds in our code Look for any missing type declarations. In
this case we were missing types from jest, express, body-parser, and supertest. Rename files from .js
to .ts
Fix errors by creating type aliases or interfaces, adding type annotations, etc. This can potentially take the most time
as you’ll need to take a look at how functions are used, how and what kind of data is passed.
One thing to keep in mind is that the migration process to TypeScript can be done at your own pace. Since valid
Javascript is also valid TypeScript, you don’t have to rename all files to .ts
. You don’t have to create type
interfaces for all objects until you’re ready. Using a less-strict tsconfig.json
file (https://github.com/tsconfig/bases/blob/main/bases/node16.json), together with // @ts-ignore
comments to ignore
errors you don’t agree with or maybe aren’t ready for yet.
After compiling
After tsc
finally compiles your project, you may need to adjust your package.json
file. In our package.json
we
have:
"main": "src/server.js",
which now points to a file that doesn’t exist. Since this should point to a Javascript file, we need to update this to
point to the compiled version:
"main": "dist/server.js",
Note that you will need to compile your app code every time your entry point needs to be updated. This can be done
automatically as you develop with packages such as tsc-watch
and nodemon.
You can also see if the compiled version of the app runs manually through a command like node dist/server.js
.
IDE Support
One of the major side effects from migrating to TypeScript, if you use a supported IDE such as Visual Studio Code or one
of the JetBrains IDEs, is better integration with the IDE. This includes, but isn’t limited to, better autocomplete and
better popup hints for function types (see image below). This brings the language much closer to typed, compiled
languages like C# and Java.
Other tooling
This post has only covered a very narrow migration case involving a basic Node.js app and a few unit tests. Real
applications are much more complex than this and would involve other transpilers and builders such as webpack, rollup,
and babel. Adding TypeScript to those build processes would be separate blog posts themselves, but I believe the general
advice here is still 100% applicable in complex situations.
Conclusion
Migrating an app to another platform or language is never easy. Fortunately, if you want to migrate your vanilla
Javascript app to TypeScript, it can be done progressively, as fast or as slow as you want. Additionally, the TypeScript
compiler can be configured to provide as much feedback as we need throughout the process to ensure your code runs.
Hopefully this post has inspired you to at least think about the possibility of migrating your Javascript app.
At Anvil, we’ve begun migrating some of our public projects to TypeScript as well. So far it’s been painless and fairly
straightforward. If you're developing something cool with our libraries or paperwork automation, let us know at
developers@useanvil.com. We'd love to hear from you.
Posted on May 9, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.