DEV Community

Cover image for Convert an Express NodeJS App From JavaScript to TypeScript
Thomas Alcala Schneider
Thomas Alcala Schneider

Posted on

Convert an Express NodeJS App From JavaScript to TypeScript

Hi! ๐Ÿ––

Today, I'll walk us through moving an Express NodeJS app from JavaScript
to TypeScript.

Why? TypeScript offers type safety "on demand", most of the code
won't break if you move your app from one to the other, and then, you
can add the safety where it is important.

How

We are going to start from a fork of Kent C. Dodds' Express example for
mid-large
apps
.
I made a branch called javascript as a starter.

Nothing is lost, nothing is created, everything is transformed

Let's change the extension of all our app's js files to ts:

$ find . -type f -name '*.js' | grep -v node_modules | grep -v babelrc | while read line; do name=$(echo $line | sed 's/\.js$/.ts/'); mv $line $name; done
Enter fullscreen mode Exit fullscreen mode

We find all js files, ignore node_modules and babelrc, and rename them
to ts.

Adding TypeScript

  1. Let's add the dependencies
$ yarn add typescript --dev
$ yarn add concurrently @types/express --dev
Enter fullscreen mode Exit fullscreen mode

And in package.json, we add more scripts:

"scripts": {
    "start": "node .",
    "build": "babel --delete-dir-on-start --out-dir dist --copy-files --ignore \"**/__tests__/**,**/__mocks__/**\" --no-copy-ignored src",
    "start:dev": "nodemon dist/index.js",
    "build:dev": "tsc --watch --preserveWatchOutput",
    "dev": "concurrently \"npm:build:dev\" \"npm:start:dev\""
  },
Enter fullscreen mode Exit fullscreen mode
  1. Init the config
$ yarn tsc --init
Enter fullscreen mode Exit fullscreen mode

You can copy my tsconfig.json, I mainly added an output dire and small things like that.

  1. Run the TypeScript compiler, crash and burn
$ yarn tsc
Enter fullscreen mode Exit fullscreen mode

So, this breaks. Now let's fix the issues

Fixing a File

Let's start with a small file: src/index.ts. It returns an error that
seems straightforward, but is representative of how TypeScript can be
annoying with little things.

Here is the content of the file:

import logger from 'loglevel'
import {startServer} from './start'

const isTest = process.env.NODE_ENV === 'test'
const logLevel = process.env.LOG_LEVEL || (isTest ? 'warn' : 'info')

logger.setLevel(logLevel)

startServer()
Enter fullscreen mode Exit fullscreen mode

And the error:

src/index.ts:7:17 - error TS2345: Argument of type 'string' is not
assignable to parameter of type 'LogLevelDesc'.

So here, we can see that logger.setLevel() is used to set the log
level, taking a logLevel variable. And it is going to be a string from
the LOG_LEVEL environment variable if defined, else based on the
NODE_ENV variable, it will be a string: 'warn' or 'info'.

HOWEVER, this crashes now, because in TypeScript, setLevel() takes
a LogLevelDesc type, which is essentially an integer with a fancy type
name.

Common libraries have types well documented, toplevel not really. So
I had to look at examples in the node_modules:

$ grep -rHin setlevel node_modules | less

node_modules/loglevel/test/node-integration.js:11:
log.setLevel(log.levels.TRACE);
node_modules/loglevel/test/node-integration.js:12:
log.setLevel(log.levels.DEBUG);
node_modules/loglevel/test/node-integration.js:13:
log.setLevel(log.levels.INFO);
node_modules/loglevel/test/node-integration.js:14:
log.setLevel(log.levels.WARN);
node_modules/loglevel/test/node-integration.js:15:
log.setLevel(log.levels.ERROR);
Enter fullscreen mode Exit fullscreen mode

... So here we have some usage, for us it is going to be
logger.levels.INFO, etc, so we replace "warn" and "info" in const
logLevel = process.env.LOG_LEVEL || (isTest ? 'warn' : 'info')
by
logger.levels.WARN and logger.levels.INFO

It's still not enough, because process.env.LOG_LEVEL is still
potentially there, and it's going to be a string. So I had to write
a function to convert the string and cast it in a LogLevelDesc:

const convertLogLevel: (logLevel: string | undefined) => logger.LogLevelDesc = (
  logLevel: string | undefined,
) => {
  switch (logLevel) {
    case "1":
    case "error":
      return logger.levels.ERROR;
    case "2":
    case "warn":
      return logger.levels.WARN;
    default:
      return logger.levels.INFO;
  }
};

const isTest = process.env.NODE_ENV === "test";
const logLevel: logger.LogLevelDesc = convertLogLevel(process.env.LOG_LEVEL) ||
  (isTest ? logger.levels.WARN : logger.levels.INFO);
Enter fullscreen mode Exit fullscreen mode

As you can see in the first line, I had to specifically write the type
of the function (logLevel: string | undefined) => logger.LogLevelDesc
(a function signature is (param1: type, param2: type, ...) =>
returnType
).

I strongly recommend that you use a linter for your editor, so you can
see type errors while writing the code.

Now that this file is fixed, let's try another one with Express code so
we see how this works for bigger, better documented libraries,

Fixing an Express Route File

Now let's fix src/routes/math.ts. There is a problem with implicit
any type for req, res, etc. This can be solved by defining an explicit
type any for those:

async function add(req: any, res: any) {}
Enter fullscreen mode Exit fullscreen mode

Types for request and stuff aren't safe and more of adding another
headache than a solution. I prefer creating a type for the query
parameters, this is more useful.

type MathQuery = {
  a: number;
  b: number;
  c: number;
};

async function add(req: any, res: any) {
  const mathQuery = req.query as MathQuery;
  const sum = Number(mathQuery.a) + Number(mathQuery.c);
  res.send(sum.toString());
}
Enter fullscreen mode Exit fullscreen mode

So here, we cast req.query as MathQuery.

Some Battles You Can't Win

We've seen well done transition to TypeScript, this latest compromise,
now we'll see a case where it is too painful to fix the code so we
ignore it.

I am a partisan of using TypeScript when it is useful, and try to use
the type system the most possible, to avoid errors at runtime.

That said, there are times when it is just too exhausting, painful and
a waste of time to use. Here for example, the src/start.ts file is
a good example. Kent has wrapped the startServer and middleware
functions in promises with no type, no real return, just a resolution.
And I'm sure he knows what he's doing a lot better than me.

There is no way to match this signature without overriding or modifying
the node type definitions, so in that case when we know it's working,
it's faster and probably best just to ignore the type verification.

Simply add // @ts-nocheck at the top of the file.

We've done it again! ๐ŸŽ‰

The final code

Top comments (0)