Navigate back to the homepage
📚 Books

Using NodeJS with TypeScript in 2020

Mark Pollmann
May 15th, 2020 · 4 min read

Note: This article is aimed at Node developers looking into TypeScript and is planned as a living document. When I find something noteworthy I will add it. If you have something cool to share, send me an email!

What TypeScript promises

Be more confident about your code and catch issues early

You will catch errors right in your editor, instead of in your tests (or production).

For example, one mistake that is quite hard to spot in plain JavaScript is not awaiting a promise in an async function:

1const myFunc = async() => {
2 try {
3 const entity = findEntityOrThrow() // oops, forgot the `await`
4
5 // TS will warn that type Promise<Entity> doesn't have a property called doEntityStuff
6 entity.doEntityStuff
7 } catch (error) {
8 [...]
9 }
10}

Self-Documenting Code

What was the order of parameters of function again? Did this function return a promise? Just hover over it in your IDE. It’s like a free JSDoc.

Basically every library is already typed for you

Either it brings its own types (because it’s written in TS or a .d.ts file was added to its package) or you can get them from DefinitelyTyped and just install via npm install -D @types/name-of-the-lib.

Use the latest JavaScript features even before they even land in Node

TypeScript is a superset of JavaScript. Which means if things get added to JS they will be added to TS as well. The TypeScript team adds features that land in the ECMAScript spec stage 3 (not the final step but pretty sure to come) pretty quickly to TS. Features like the nullish coalescing operator were added in less than a month after anouncement.

Faster Onboarding of New Teammates

New developers can understand your codebase quicker (self-documenting code) and are less afraid of checking in faulty code (the static types will give them fewer chances to do so).

Choose your own sweet spot between strictness and agility for your team

Don’t believe the purists: You don’t have to write perfect TS from the start. Allow escape hatches from the type system via explicit or implicit anys, unknown or ignoring specific lines with @ts-ignore comments.

“TypeScript slows us down”

If TS refusing to compile is bringing you down you can decouple type-checking from code generation. Meaning you tell TS to not emit any code (--noEmit = true), just give some helpful advice and let babel convert your TS to JavaScript. This way TS will never refuse to compile and you can check in your code even if it’s not perfect.

It’s Open Source

Not a unique selling point nowadays but still, it’s developed on GitHub, you can just look at the issues in the repo if you encounter an issue or read their roadmap to get a sense of what’s coming.

What does it not do?

TS annotations don’t (and probably never will) change your JavaScript code. At compile time all TypeScript specific annotations are removed and the JS code is emitted as is. It can downcompile your code for earlier environments, though. (see the target compiler option). This is more interesting in a browser environment but still good to know.

I’m convinced, let me play with it!

You can get a feel for the language (and its generated output) at the TypeScript playground.

Best Practices

Be specific

TS has literal types where you allow only some specific values instead of the more general types like number or string. So if you want to only allow specific values you can do so. Let’s say you have a function that expects only one of three values: ‘running’, ‘stopping’, and ‘stopped’:

1// ❌ too generic
2function myFunc (mode: string) {...}
3
4// ✅ catches typos like 'runing'
5type mode = 'running' | 'stopping' | 'stopped'
6
7function myFunc (mode: Mode) {...}
8// also you might decide to change 'running' to 'isRunning'
9// TS will tell you if you forget to update it somewhere in your codebase

Don’t be too explicit

You don’t have to annotate every variable. That actually introduces noise and can hinder type inferring. Try to let the types flow through the codebase.

1let isRunning: boolean = false // unnecessary annotation
2let isRunning = false // boolean is inferred
3
4let counter: number = 0 // unnecessary annotation
5let counter = 0 // number is inferred
6
7const myArray: MyCustomObject[] = ...
8// 👇 unnecessary annotation of obj, gets inferred
9// If you change the type of myArray later the explicit annotation would be wrong
10myArray.map((obj: MyCustomObject) => {...})

Note, though, that the inferred types are not the strictest types that are possible (these would be type false for isRunning and type 0 for counter. That would mean you couldn’t toggle isRunning to true or increment counter, though, which is not what we want). This is called type widening.

When to use any vs unknown

unknown is the type-safe alternative to any and should be preferred. If you declare a variable as unknown the compiler doesn’t allow you to do anything with it until you checked its type:

With any you’re free to do any mistake you want:

1const myBanana: any = somethingFromAnApiForExample
2
3// TS: Sure, bro, let me just crash if it's not a string 😈
4myBanana.toUpperCase()
5
6// TS: If you think that's a good idea...
7myBanana.thisMethodDoesNotExist()
8
9// TS: check your own grammar
10myBanana.touppercasee()

With unknown TypeScript has your back:

1const myBanana: unknown = somethingFromAnApiForExample
2
3// I'm pretty sure this is a string
4myBanana.toUpperCase() // TypeScript: ❌ "pretty sure" is not good enough
5
6if (typeof myBanana === 'string') {
7 myBanana.toUpperCase() // TypeScript ✅ now we know it really is a string
8
9 // error caught in compile-time
10 myBanana.thisMethodDoesNotExist()
11
12 // error caught in compile-time
13 myBanana.touppercasee()
14}

Type Your Promises

When writing a promise try to be as specific as possible regarding the type of the promise.

1// ❌ Inferred return type is `Promise<unknown>`
2const myApi = () => {
3 return new Promise((resolve, reject) => {
4 resolve(1)
5 })
6}
7
8// ✅ Now we know it's a `Promise<number>`
9const myApi: () => Promise<number> = () => {
10 return new Promise((resolve, reject) => {
11 resolve(1)
12 })
13}

esModuleInterop

You should turn on the esModuleInterop compiler option. If you don’t, you will get problems when you try to default import some libraries that were written before the ES Modules spec. Because this spec does not allow the default export to be callable. An example would be Express:

1import express from 'express'
2const app = express()

Which results in the the following error:

1Module '"[...]/node_modules/@types/express/index"' can only be default-imported using the 'esModuleInterop' flag
2
3index.d.ts(116, 1): This module is declared with using 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.

Don’t fall into traps with Object, object and {}

If you type a variable with {} it doesn’t mean it’s an object like you know them from JavaScript. It is called the empty type and it represents a value that has no members of its own.

1let a: {}
2
3a = {} // ✅
4a.name = 'hi' // ❌ Property 'name' does not exist on type '{}'.
5a = [] // ✅
6a = 'hi' // ✅
7a = 5 // ✅
8a = false // ✅

Similar with object: It’s not an object as you might expect but represents all non-primitive types (Primitive types are null, undefined, string, boolean, number, bigint and symbol).

1let b: object
2
3b = {}
4b = []
5b = 'hi'
6b = 5
7b = false

The Object type describes functionality of all JavaScript objects. Like all capitalized general types it is to be avoided, though.

Use Advanced Types

They can look intimidating but most are actually quite straight-forward.

Take a look at the Omit type for example. We take a type or interface and remove properties from it:

Example of Omit type

Now take the following code of a tweet of Michael Jackson:

Tweet about type of React Router

He’s removing the href attribute and adding three more properties to his own type. Looks super-complex but is actually quite straightforward.

Another type is Partial. It takes a type or interface and makes all properties optional:

Example of Partial type

Other examples would be Readonly<T> to make all properties (you guessed it) readonly or Pick<T, K extends keyof T> which lets you pluck some types from another type.

You can read more about advanced types here.

Type or Interface?

Both are pretty interchangable for simple types:

1type MyType = {
2 firstName: string
3 lastName: string
4 isAdmin: true
5}
6
7interface MyInterface {
8 firstName: string
9 lastName: string
10 isAdmin: true
11}
12
13type MyFunc = (x: string) => number
14
15interface MyFunc2 {
16 (x: string): number
17}

You can also use generics with both and classes can implement both.

I prefer types, though, as you can implement unions, intersections, tuples and arrays easier:

1type MyUnion = 'running' | 'stopped'
2
3type MyIntersection = SomeTypeA & SomeTypeB
4
5type MyTuple = [number, number]
6
7type MyArray = string[]

Don’t Prefix your Interfaces with I or your Types with T

You will see some types in the wild like TProps or IRoute. These prefixes are unnecessary and nowadays considered bad style.

Express

No need to type middleware functions

1import express, { Request, Response } from 'express'
2const app = express()
3
4// ❌ unnecessary annotations
5app.get('/', (req: Request, res: Response) => {
6
7})
1import express from 'express'
2const app = express()
3
4// ✅ types Request and Response get inferred
5app.get('/', (req, res) => {
6
7})

If you can’t inline your middleware you can type the function itself instead of the parameters:

1// ❌ Not wrong but lots of noise
2import express, { Request, Response, NextFunction } from 'express'
3const myMiddleware = (req: Request, res: Response, next: NextFunction) => {}
4
5// ✅
6import express, { Handler } from 'express'
7const myMiddleware: Handler = (req, res, next) => {}

Further Reading

More articles from Mark Pollmann

Migrating a React Codebase to TypeScript

Switching to TypeScript with create-react-app.

May 5th, 2020 · 3 min read

TypeScript 3 - What you might have missed

const assertions, the unknown type, optional chaining and more.

January 9th, 2020 · 1 min read
© 2017–2021 Mark Pollmann
Link to $https://twitter.com/MarkPollmannLink to $https://github.com/MarkPollmannLink to $https://www.linkedin.com/in/mark-pollmann-961446132/