The Guide to Kickstarting Backend TypeScript Development

Tomas Nilsson
JavaScript in Plain English
15 min readJun 8, 2021

--

Photo by Oskar Yildiz on Unsplash

This article requires you to have at least some TypeScript knowledge. Topics like ReturnType, Partial, Generics <T> , intersections, how to modify the global namespace, and the tsc command line will be covered. All the while focusing on using the type of safety that TypeScript offers.

Let’s begin with establishing a baseline on a couple of things.

Running vs developing

This statement is something I’d wished someone had been more specific on when starting to learn TypeScript. Writing the code and running the code is two very different things. TypeScript only exists while developing and what’s really running is plain JavaScript. This makes debugging harder and using types while in runtime is impossible (except for the ones supported by vanilla JavaScript).

tsconfig.json recommendations

Keep the settings as tight as possible and let TypeScript shine. This especially includes noImplicitAny: true and strictNullChecks: true . Keeping the settings tight seem annoying at times, but for the long run — they’re a winning concept. Fewer bugs, better IntelliSense, and refactoring becomes a walk in the park.

Target

There is a number of variations on target , se documentation at https://www.typescriptlang.org/tsconfig/#target. For backend development, there’s no need to consider browser compatibility. Just use the latest and greatest features.

{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs", /* 'commonjs', 'es2020', or 'ESNext'. */
"outDir": "./out",
"rootDir": "./src",
"strict": true,
"esModuleInterop": false,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowUnreachableCode": true,
"noImplicitAny": true,
"strictNullChecks": true,

"strictPropertyInitialization": true,
"alwaysStrict": true,
"noImplicitThis": true,
"strictBindCallApply": true,
"experimentalDecorators": true,
"incremental": true,
"noUncheckedIndexedAccess": true
}
}

Defining a simple type

Before diving in to the interesting details, let's just take a brief look at how to define a simple type:

type User = {
Name: string,
AccessLevel: "NewUser" | "User" | "Admin"
}

To use it, just reference the type with name Useras in this code:

function Login(userName: string, pw: string) : User | undefined {    let theUser: User | undefined = undefined;    if (userName === "root" && pw=== "root")
theUser = { Name: "Root", AccessLevel: "Admin" };
return theUser;
}

Alright, with that out of the way, let’s jump into business.

Known type vs “any” vs “unknown”

any

Objects that are defined as any can be considered to be a “JavaScript object”. In essence, all operations are allowed and unchecked (assigning values, deleting values, and others). Consider it to be the programmer's decision to say “I don’t know anything about this variable, and it’s ok to access, delete and modify any values on it without any type checking”.

any is really useful when migrating from JavaScript to TypeScript, but try to stay clear of it. If there’s a must for it, change tsconfig.json , setting noImplicitAny to false to decrease the number of errors.

let a: any = {
Foo: "1"
};
a = { Foo: "100", Fruit: "Banana" }; // No problema.Bar = "2"; // No problem
delete a.Foo; // No problem
console.log(a.Foo); // Returns undefined

unknown

The unknown type is somewhat different. It’s possible to assign whatever you like (as with any), but trying to access/modify the values gives an error.

let a: unknown = {
Foo: "1"
};
a = { Foo: "100", Fruit: "Banana" }; // No problema.Bar = "2"; // Returns error
delete a.Foo; // Returns error
console.log(a.Foo); // Returns error

never

never doesn’t appear in good written TypeScript, but it might when converting from JavaScript to TypeScript. Consider the following scenairo:

const Order = {
OrderID: "1234",
OrderRows: []
};
Order.OrderRows.push({ ArticleID: 123 });

Since there are no types defined on the Order object, TypeScript will try to figure out how the object looks like. Unfortunately, the OrderRows is just an array, but the type for it cannot be determinated. Because of this, the type will be set to never .

Resolving the issue is easy, just specify the type of the object:

const Order : { OrderID: string, OrderRows: {ArticleID: number}[] }= {
OrderID: "1234",
OrderRows: []
};
Order.OrderRows.push({ ArticleID: 123 });

Getting to the type…

Now in theory all objects are well defined and the type is known. In practice, most (all?) objects which are loaded from disk or fetched remotely are not. As a developer, this brings a challenge as (for example) a get request to fetch data returns an object of some kind that needs to be adapted to TypeScript.

There are (at least) four methods to assign a type to an object:

Method 1: Type assertions (or casting if you prefer c# syntax)

const myObject = <TheTargetType>request("http://...");// orconst myObject = request("http://...") as TheTargetType

Method 2: Declare a variable with a specific type

const myObject : TheTargetType = request("http://...");

Method 3: Use User Defined Type Guards

User-Defined Type Guards are functions that examine an object, and if it passes set requirements, it’s considered to be of a certain type.

// The type
type Person = { Name: string, Age: string }
// User Defined Type Guard
function isPerson(object: unknown): object is Person {
// Rules: Name and Age must exist in in the object.
const keys = Object.keys(data);
return (["Name", "Age"].every(v => keys.includes(v)));
}
// The unknown data (gotten from somewhere)
const rawData = `{"Name":"Sarah","Age":30}`;
const data = JSON.parse(rawData); // type is "any"
// Using the type guard
if (isPerson(data)) {
// data is of type Person (inside this closure)
console.log(`name: ${data.Name}, age ${data.Age}`);
}

Method 4: Generics

Generics especially useful when loading remote data from for example a website. Since generics are so powerful, there’s a dedicated chapter to it below.

async function FetchRemoteSite<T>(url: string): Promise<T> {
const result = await axios.get(`https://site.com/api/${url}`);
return result.data as T;
}

typeof

typeof in classic JavaScript is used to get the name of the type for an object or primitive (ie “object”, “string”, “number” and so forth)

const a = { Foo: "Bar" };
const b = "Bar";
console.log(typeof a); // Returns "object"
console.log(typeof b); // Returns "string"

It’s not possible to use typeof to get the TypeScript type, because all running code is JavaScript and the type doesn’t exist.

type FooType = { Name: string, Age: number };
const foo: FooType = { Name: "A", Age: 1 };
console.log(typeof foo); //"FooType"? No! returns "object"

What can typeof in TypeScript be used for then? Well, it can copy existing types of other objects and functions. This is for example valid code:

let x: { id: number };const y: typeof x = { id: 7 };

A bigger example could be:

const DefaultConfig = {Foo: 1, Bar: 44 };function LoadFromDisk(): typeof DefaultConfig {    // Copy the type
let result: typeof DefaultConfig | undefined = undefined;
result = fs.existsSync("config.json")
? JSON.parse(fs.readFileSync("config.json", "utf-8"))
: DefaultConfig;
return result!;
}

ReturnType

Consider the following code. The return of DoWork is { a: string, b: string} | undefined and the function must somehow build that result. That might lead to declaring the same object again (see line 3)

function DoWork(): { a: string, b: string } | undefined {const result: { a: string, b: string } | undefined = undefined;
/* ... additional code here */
return result;
}

There are several ways to resolve this, and the most obvious one is to declare a separate type:

type WorkResult = { a: string, b: string };function DoWork(): WorkResult | undefined {
const result: WorkResult | undefined = undefined;
/* ... additional code here */
return result;
}

But there are other ways as well.

Remote the return value from the function

Remove the return information from the function line and allow TypeScript to do its magic. When hovering over DoWork TypeScript knows about the return type. Unfortunately, this decreases readability and, from time to time, TypeScript doesn’t produce a ReturnType that’s sharp enough (or even correct).

Use ReturnType

With ReturnType , it’s possible to reference a function and extract the type it’s returning.

Example 1:

function DoWork(): { a: string, b: string} | undefined {    let result: ReturnType<typeof DoWork> = undefined;    /* Code that populates the result variable */
result = { a: "a", b: "b" }
return result;}DoWork();

Example 2:

It might be even more useful if you want to get hold of a type from a module you don’t control. Consider the dependecy of a badly coded database implementation. This way you can guarantee that the type returned will be identical to the type of the referenced module.

import { LoadDatabaseEntry } from './badlycoded';function Trimname(): ReturnType<typeof LoadDatabaseEntry> {    const entry = LoadDatabaseEntry();
entry.Name = entry.Name.trim();
return entry;
}

Combing ReturnType with Omit or Pick gives even more control:

import { LoadDatabaseEntry } from './badlycoded';
function Trimname(): Pick<ReturnType<typeof LoadDatabaseEntry>, "Name" | "Age"> {
const entry = LoadDatabaseEntry();
entry.Name = entry.Name.trim();
return entry;
}
Trimname() // Contains only "Name" and "Age"

Multiple returntypes (Union)

It’s possible to return for example an object with relevant data (on success) or an error object (with relevant error data) as follows:

function LoadData(): { Name: string, Age: number } | { Error: string } {
// Simulate a call that succeeds 90% of the time
if (Math.random() > 0.1)
return { Name: "John", Age: 27 };
return { Error: "Could not load data." };}

Returning different data on success and fail — in more detail:

This feature is both good and bad. It will improve on code readability, and it will (at the same time), decrease it. Keep on reading. :)

Consider this. On Success: true , the relevant data is returned. On Success: false , no data is returned:

function LoadData() {      // Simulate a call that succeeds 90% of the time
if (Math.random() > 0.1)
return { Success: true, Name: "John", Age: 27 };
return { Success: false };
}

Hovering the function, it reveals that TypeScript has built the correct return type:

Now, let’s change to this:

function LoadData() : { Success: true, Name: string, Age: number} | { Success: false} {
...
}

TypeScript has been instructed to be aware there’s a difference in the returned data (depending on the result of Success ). Hovering now reveals:

With IntelliSense; since I’ve checked the data.Success to be true, then I have access to Age and Name values (and only then).

What’s bad with this approach then? Well, it’s just that. In the below example there’s no check that Success is true, and since TypeScript cannot perfectly decide what to display, the Name and Age values are hidden. This hides away important information for the developer.

Returning multiple values

This is not a TypeScript thing, but it’s good to be aware of.

function MultipleReturns1() {
return { Foo: "Yes", Bar: "No" };
}
const { Foo, Bar } = MultipleReturns1(); // Destructuring
console.log(`Foo=${Foo}, Bar=${Bar}`); // Prints "Foo=Yes, Bar=No"

and with arrays:

function MultipleReturns2() {
return ["Yes", "No"];
}
const [a, b] = MultipleReturns2();
console.log(`${a} ${b}`); // Prints "Yes No"

Intersections / “merge” types

It’s possible to “merge” two types into a new one. In TypeScript this is described as “Extending a type via intersections”.

type User = { Name: string, Age: number }function LoadData(): User & { IsNewUser: boolean } {   const n = Math.random() > 0.2;
return { Name: "John", Age: 27, IsNewUser: n};
}const data = LoadData();
console.log(data.IsNewUser); // OK!

Generics <T>

Generics is identified by the type parameter and it’s often identified with the single letter T as in function LoadData<T>() { ... }.

The easiest way to explain it is: Rather than the function deciding on the type to be returned (ie, “If you call this function it will function return a User-object”), the caller decides on the object to be returned. Basically, the type is a parameter to the function.

When it comes to functions written for loading data from disk, database, or remote (http://…), then generics is most likely something to consider.

Example:

type User = { Name: string, Age: number }async function LoadData<T>(url: string): Promise<T> {
const result = await axios.get(`https://site.com/api/${url}`);
return result.data as T;
}
async function Main() { const u1 = await LoadData("users"); // u1 is unknown :(
const u2 = await LoadData<User[]>("users"); // u2 is User[]
}

Depending on function, the Type parameter can be omitted:

function Reverse<T>(data: T[]): T[] {
// For the sake of demo only
return data.reverse();
}
const sourceData = ["A", "B", "C"];
console.log(Reverse(sourceData)); // Output = [ 'C', 'B', 'A' ]

enums

Enums in its most basic form is declared as the following:

enum FileAccess {
Read, // Will become the number 0
Write // Will become the number 1
}
const p: FileAccess = FileAccess.Read;

Enums that will be stored in a database or similar need to be permanently fixed for the lifetime of the application.

// Declare the enum
enum eFileAccess {
Read = "ACCESS_READ",
Write = "ACCESS_WRITE"
}
// Use it
const p: eFileAccess = eFileAccess.Read;
if (p === eFileAccess.Read)
console.log("Read access graned");
console.log(p); // Returns "ACCESS_READ"// Convert from string to enum
const f = <eFileAccess>"ACCESS_WRITE";
if (f === eFileAccess.Write)
console.log("Write access granted");

Bitwise enum / Flags (from c#)

Flags aren’t really supported, but can easily be fixed with some bitwise operations.

enum eFileAccess {
Read = 1 << 0, // 0001 = 1
Write = 1 << 1, // 0010 = 2
ReadWrite = Read | Write // 0011 = 3
}
const p: eFileAccess = eFileAccess.ReadWrite;if (p & eFileAccess.Write) // Bitwise and
console.log("Write access graned");

Go Readonly!

There are several ways do to this.

Method 1: Define a type where readonly is used:

type Position = {
readonly x: number;
readonly y: number;
}
const a: Position = { x: 1, y: 5 };
a.x = 10; // Error

Method 2: Turn an existing type into readonly.

type Position = {
x: number;
y: number;
}
function GetPosition(): Readonly<Position> { // Returns readonlyconst result: Position = { x: 0, y: 0 };
result.x = 10; // Writable
result.y = 20;
return result;
}let a = GetPosition();
a.x = 10; // Error

Index Signature / the “good” any / KeyValuePair

any should most likely be avoided, but sometimes there’s the need for storing data in “key” and “value” pairs. The Index signatures allows for that.

const kv: { [key: string]: string | number } = {};
kv.Name = "Sarah";
kv.Age = 47;
console.log(JSON.stringify(kv)); // {"Name":"Sarah","Age":47}

Looping an index signature with type safety is as easy as:

for (const [key, value] of Object.entries(kv))
console.log(`key=${key}, value=${value}`);

Finally, if the index signature should be read-only, do the following:

type rokv = { readonly [x: string]: number; }const foo: rokv = { "Hour": 3, "Minute": 29 };console.log(foo.Hour);   // Returns 999foo.Minute = 12;         // Not allowed, is read only

And of course, it’s very possible to have objects as values with IntelliSense.

Interfaces? Or Types?

They are almost identical, both in terms of how they’re used and how they’re written. It’s such a personal preference, with few exceptions. With some spaces to align them they compare as:

interface Person   { Name: string, Age: number }
type Person = { Name: string, Age: number }

Extending both types is possible, but the syntax differs. Read more at https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces

A significant difference is that Interfaces with the same name are merged! This is especially important when there’s a need for modifying already existing interfaces (keep on see “Modifying the global namespace” below).

interface Person { Name: string }
interface Person { Age: number }
const newUser: Person = { Name: "Linda", Age: 80 }; // OK!

Modifying the global namespace (global.name)

On occasion, there’s this need for having a global variable available across your entire Node.js project. It might be a constant, a function, or a logger.

Globals is defined in .d.ts files, as this one declarations.d.ts. Place it under “src”.

Important! Set the environment variable TS_NODE_FILES to the word “true”. This will enable the include/ exlude config settings in tsconfig.json

declare global {
namespace NodeJS {
interface Global {
LicenseKey: string
}
}
}
export { }; // Don't forget this

:!: Got “ES2015 module syntax is preferred over custom TypeScript modules and namespaces”? Do not use .ts naming, use .d.ts .

To access the global variable, function or object, you just need to:

function Main() {
global.LicenseKey = "ABC123";
}

It’s possible to declare the variable on the very root. An example would be to have a logger object globally, which is accessible without including it in every file. For this scenario there are a couple of things to consider:

declarations.d.ts:

type Logger = {
debug: (p: string) => void
}
declare global { // eslint-disable-next-line no-var
declare var logger: Logger;
}
export { };

The biggest challenge with global objects such as this logger is that it requires super early initialization. Make an index.ts which initializes needed global variables, and only that. This file in turn includes a single file, like main.ts , where the rest of init runs. Done right, the index.tswill be able to run its code before including the main.ts file.

# Init my (fake) global logger (note: without global-prefix)
logger.debug = (iData: string) => { console.log(iData); };
# Use it
logger.debug("Logger initialized, booting system.");

Environment variables and the global namespace

Finally, let’s investigate how to modify the global namespace to get type check on environment variables. The process.env.VARIABLENAME can be configured as:

declare global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "production" | "development",
VARIABLENAME: string | undefined,
}
}
}
export { };

Usage:

if (process.env.NODE_ENV === "development")
console.log(`VARIABLENAME=${process.env.VARIABLENAME}`);

Modifying the Express namespace

More info on using global variables

I’ve written an additional article on the matter, read it here.

Modifying the Express namespace

import "express"; // Modifies global namespace, so include it!declare global {namespace Express {
interface Request {
token: string,
userId: string
}
}
}
export { };

Add middleware to decode our user (from bearer, cookie or similar) and store the verified user in the req object. It’s possible to store just the ID of the user, or an actual object with more data.

import * as Express from "express";app.use((req: Express.Request, res: Express.Response, next: Express.NextFunction) => {    // Do work to decode req.headers.authorization here
req.token = "thetoken";
req.userId = "usr00001";
next();});

In your routes, it’s as easy as:

app.get(`/test`, async function (req: Express.Request, res: Express.Response) {   if (!req.userId)
// ... Not authenticated
res.status(401).json({ Success: false });
else
// ...All ok
res.json({ Success: true });
});

Promise vs async

Let’s begin with establishing that for most use cases they can be considered to be identical. Below are two functions, one returns a new Promise and the other uses async (with type safety). These two functions are called in four different ways.

// Two different implementations doing the same thingasync function DoSomething(): Promise<number> {
return Math.random();
}
function DoSomething2() {
return new Promise<number>((resolve: (result: number) => void) => {
resolve(Math.random());
});
}// Test functionasync function TestThemAll() {// Test both using ".then" syntaxDoSomething().then(result => {
console.log(`DoSomething returned: ${result}`);
});
DoSomething2().then(result => {
console.log(`DoSomething2 returned: ${result}`);
});
// Test them both using "await" syntaxconsole.log(`DoSomething returned: ${await DoSomething()}`);
console.log(`DoSomething2 returned: ${await DoSomething2()}`);
}TestThemAll();

… but what about callbacks? … please. No.

Starting a TypeScript project as-is

There is no need to transpile to out and run the code from there, it can be done on the fly (even with breakpoints and stepping in VSCode).

Start a TypeScript project from the command line:

set TS_NODE_FILES=true & node -r ts-node/register src/index.ts

In VSCode, there needs to be a .vscode\launch.json file. Add add the following and press F5 to run.

{
"version": "0.2.0",
"configurations": [
{
"name": "Debug typescript",
"type": "node",
"request": "launch",
"smartStep": false,
"sourceMaps": true,
"args": ["${workspaceRoot}/src/index.ts"],
"runtimeArgs": [
"-r",
"ts-node/register/transpile-only"
],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"internalConsoleOptions": "openOnSessionStart",
"env": {
"TS_NODE_IGNORE": "false",
"TS_NODE_FILES": "true", // Respect include/exclude in tsconfig.json => will read declaration files
"NODE_ENV": "development"
},
"skipFiles": [
"<node_internals>/*",
"<node_internals>/**",
"<node_internals>/**/*",
"${workspaceRoot}/node_modules/**",
"${workspaceRoot}/node_modules/**/*"
],
"stopOnEntry": false,
"outputCapture": "std",
},
]
}

If you want to change what to run when pressing F5, open the vscode command palette (ctrl-shift-p) and search for Debug: Select and start debugging .

Some tool tips!

Finally getting to the end. Lets wrap up with some misc tool tips.

tsc

Running tsc while developing will help to track errors on the project level. VSCode in combination of eslint and the Error Lens plugin will show local errors.

Building once

Open a terminal and type tsc -b -v to build the project a single time (with verbose on). The current working directory should be the same folder as tsconfig.json.

Building continuously (watch & incremental)

Much better is to turn on watch for file changes -w and, to that, use incremental -i for faster transpiling. All in all use:

tsc -b -v -i -w

typesync

Not all modules found on npmjs.com has types built-in. typesync scans the project and tries to find needed types:

📦 project1 — package.json (1 new typings added, 0 unused typings removed)
└─ + @types/express

In the cases where no types exist in the module itself or there are no separate ones, then you are on your own. Either write the definitions manually or use declare module to set the module as any. For doing the latter, create a definition.d.ts with the following contents:

declare module '<module name here>';// Example:
declare module 'rfc822-validate';

npm-check

Use npm-check tool to upgrade your dependencies. Run it with npm-check -d or npm-check -d -g

deadfile

Finding unused files is somewhat tricky, because there are so many ways to use files. deadfile does a decent job.

deadfile .\\src\\index.ts --exclude out --exclude log

Enjoy :)

More content at plainenglish.io

--

--