Converting your vanilla Javascript app to TypeScript

useanvil

Anvil Engineering

Posted on May 9, 2022

Converting your vanilla Javascript app to TypeScript

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
Enter fullscreen mode Exit fullscreen mode

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__/**/*"
  ]
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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')
                             ~~~~~~~~~~~~~
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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]
                  ~~~~~~~~~~~~~~~~
Enter fullscreen mode Exit fullscreen mode

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 = {}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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",
Enter fullscreen mode Exit fullscreen mode

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",
Enter fullscreen mode Exit fullscreen mode

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.

IDE Support(./ide-support.png)

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.

💖 💪 🙅 🚩
useanvil
Anvil Engineering

Posted on May 9, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related