Engineering

Converting your vanilla Javascript app to TypeScript

Headshot, Allan Almazan
By Allan Almazan

Start converting your vanilla Javascript app to TypeScript with a few easy steps

Back to all articles
Converting your vanilla Javascript app to TypeScript
Engineering

Converting your vanilla Javascript app to TypeScript

Headshot, Allan Almazan
By Allan Almazan

Start converting your vanilla Javascript app to TypeScript with a few easy steps

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.

IDE Support

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.

Giant Bottom Asset
Desktop Bottom Asset
Tablet Bottom Asset
Mobile Bottom Asset
Giant Top Asset
Desktop Top Asset
Tablet Top Asset
Mobile Top Asset