c

Typing the express app

Now that we have a basic understanding of how TypeScript works and how to create small projects with it, it's time to start creating something actually useful. We are now going to create a new project with a bit more realistic use cases in mind.

One major change from the previous part is that we're not going to use ts-node anymore. It is a handy tool and helps you get started, but in the long run it is advicable to use the official TypeScript compiler that comes with the typescript npm-package. The official compiler generates and packages JavaScript files from the .ts files so that the built production version won't contain any TypeScript code anymore. This is the exact outcome we are aiming for, since TypeScript itself is not executable by browsers or Node.

Setting up the project

We will be creating a project for Ilari, who loves flying small planes but has a difficult time managing his flight history. He is quite the coder himself, so he doesn't necessarily need a user interface, but he'd like to use the software with HTTP-requests and retain the possibility to later add a web-based user interface to the application.

Let's start by creating our first real project 'Ilari's flight diaries'. As usual run npm init and install the typescript package.

TypeScript's native tsc compiler can help us to initialize our project with the command tsc --init. First we need to add the tsc command to the list of executable scripts in the package.json file (unless you have installed typescript globally). Even if you have installed TypeScript globally, you should always include it as a dev-dependency in your project.

The npm script for running tsc is set as follows:

{
  // ..
  "scripts": {
    "tsc": "tsc",  },
  // ..
}

Often the bare tsc command is added to the scripts for other scripts to use, so it is common to see the tsc command set up within the project like this.

Now we can initialise our tsconfig.json settings by running:

 npm run tsc -- --init

Notice the extra -- before the actual argument! Arguments before the -- are interpreted for the command npm and ones after are for the command that is run through the script.

Running the script creates an tsconfig.json file, which contains a lengthy list of every configuration available to us. However only a few have not been commented out. Studying the initial tsconfig.json file might be useful for finding some configuration options you might need. It is also completely okay to keep the commented rows in the file just in case you might someday need to expand your configuration settings.

The settings we want right now are the following:

{
  "compilerOptions": {
    "target": "ES6",
    "outDir": "./build/",
    "module": "commonjs",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true
  }
}

Let's go through each configuration:

The target configuration tells the compiler which ECMAScript version to use for the generated JavaScript. ES6 is supported by most browsers, and it is a good, pretty safe option.

outDir tells where the compiled code should be placed.

module tells the compiler we want to use commonjs modules in the compiled code. This means we can use require instead of import, which is not supported in older Node.js versions such as the version 10.

strict is actually a shorthand for multiple separate options:

noImplicitAny, noImplicitThis, alwaysStrict, strictBindCallApply, strictNullChecks, strictFunctionTypes and strictPropertyInitialization. These guide our coding style to use the TypeScript features more strictly. For us perhaps the most important is the already familiar noImplicitAny. It prevents implicitly setting type any, which can happen if you don't type the parameters of a function for example. Details of the rest of the configurations can be found from the tsconfig documentation. Using strict is suggested by the official documentation.

noUnusedLocals prevents having unused local variables, and noUnusedParameters throws an error if a function has unused parameters.

noFallthroughCasesInSwitch ensures that in a switch case each case ends with a return or a break statement.

esModuleInterop allows interoperability between commonJS and ES Modules, see more in documentation.

Now that we have our preferred configuration set, let's continue by installing express and of course also @types/express. Since this is a real project, which is intended to be grown over time, we will use eslint from the very beginning:

npm install express
npm install --save-dev eslint @types/express @typescript-eslint/eslint-plugin @typescript-eslint/parser

Now our package.json should look something like this:

{
  "name": "ilaris-flight-diaries",
  "version": "1.0.0",
  "description": "",
  "main": "index.ts",
  "scripts": {
    "tsc": "tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.2",
    "@typescript-eslint/eslint-plugin": "^2.17.0",
    "@typescript-eslint/parser": "^2.17.0",
    "eslint": "^6.8.0",
    "typescript": "^3.7.5"
  }
}

We also create .eslintrc with the following content:

{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking"
  ],
  "plugins": ["@typescript-eslint"],
  "env": {
    "browser": true,
    "es6": true
  },
  "rules": {
    "@typescript-eslint/semi": ["error"],
    "@typescript-eslint/explicit-function-return-type": 0,
    "@typescript-eslint/no-unused-vars": [
        "error", { "argsIgnorePattern": "^_" }
    ],
     "@typescript-eslint/no-explicit-any": 1,
    "no-case-declarations": 0
  },
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json"
  }
}

Now we just need to set up our development environment, and we are ready to start writing some serious code. There are many different options for this. We could use the familiar nodemon with ts-node, but as we saw earlier, ts-node-dev does the exact same thing and we can continue using it. So, let's install ts-node-dev

npm install --save-dev ts-node-dev

And we are ready to start writing some code after defining still a couple of more npm scripts:

{
  // ...
  "scripts": {
    "tsc": "tsc",
    "dev": "ts-node-dev index.ts",    "lint": "eslint --ext .ts ."  },
  // ...
}

There is a lot of stuff to go through before you can start actual coding. When you are working with a real project, careful preparations support your development process a great deal. Take the time to create a good setting for yourself and your team so in the long run everything runs smoothly.

Let there be code

Now we can finally start coding! As always, we start by creating a ping-endpoint, just to make sure everything is working.

The contents of the index.ts file:

import express from 'express';
const app = express();
app.use(express.json());

const PORT = 3000;

app.get('/ping', (_req, res) => {
  console.log('someone pinged here');
  res.send('pong');
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Now if we run the app with npm run dev we can verify that a request to http://localhost:3000/ping gives a response pong, so our configuration is set!

When starting the app with npm run dev, it runs in development mode. The development mode is not suitable at all when we later operate the app in production.

Let's try to create a production build by running the TypeScript compiler. Since we have defined the outdir in our tsconfig.json, there's really nothing else to do but run the script npm run tsc.

Just like magic a native runnable JavaScript production build of the express backend is created into the directory build.

Currently, if we run eslint it will also interpret the files in the build directory. We don't want that, since the code there is compiler generated. We can prevent this by creating a .eslintignore file which lists the content we want eslint to ignore, just like we do with git and gitignore.

Let's add an npm script for running the application in production mode:

{
  // ...
  "scripts": {
    "tsc": "tsc",
    "dev": "ts-node-dev index.ts",
    "lint": "eslint --ext .ts .",
    "start": "node build/index.js"  },
  // ...
}

When we run app with npm start we can verify that also the production build works

15a

Now we have a minimal working pipeline for developing our project. With the help of our compiler and eslint it also ensures that a good code quality is maintained. With this base we can actually start creating an app that we could later on deploy into a production environment.

Implementing the functionality

Finally we are ready to start writing some code.

Let's start from the basics. Ilari wants to be able to keep track of his experiences on his flight journeys.

He wants to be able to save diary entries which contain:

  • The date of the entry
  • Weather conditions (good, windy, rainy or stormy)
  • Visibility (good, ok or poor)
  • Free text detailing the experience

We have obtained some sample data, which we will use as a base to build on. The data is saved in json format, and can be found from here.

The data looks like the following

[
  {
    "id": 1,
    "date": "2017-01-01",
    "weather": "rainy",
    "visibility": "poor",
    "comment": "Pretty scary flight, I'm glad I'm alive"
  },
  {
    "id": 2,
    "date": "2017-04-01",
    "weather": "sunny",
    "visibility": "good",
    "comment": "Everything went better than expected, I'm learning much"
  },
  // ...
]

Let's start by creating an endpoint which returns all flight diary entries.

First we need to make some decisions on how to structure our source code. It is better to place all source code under src directory, so source code is not mixed with configuration files. We will move index.ts there and make the necessary changes to the npm scripts.

We will place all routers, modules which are responsible for handling a set of specific resources such as diaries, under the directory src/routes. This is a bit different than what we did in part 4, where we used directory src/controllers.

The router taking care of all diary endpoints is in src/routes/diaries.ts and looks like this:

import express from 'express';

const router = express.Router();

router.get('/', (_req, res) => {
  res.send('Fetching all diaries!');
})

router.post('/', (_req, res) => {
  res.send('Saving a diary!');
})

export default router;

We'll route all requests to prefix /api/diaries to that specific router in index.ts

import express from 'express';
import diaryRouter from './routes/diaries';const app = express();
app.use(express.json());

const PORT = 3000;

app.get('/ping', (_req, res) => {
  console.log('someone pinged here');
  res.send('pong');
});

app.use('/api/diaries', diaryRouter);

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

And now if we make a HTTP GET request to http://localhost:3000/api/diaries we should see the message Fetching all diaries!.

Next we need to start serving the seed data (found here) from the app. We will fetch the data and save it to data/diaries.json.

We won't be writing the code for the actual data manipulations on the router. We will create a service which takes care of the data manipulation instead. It is quite common practice to separate the "business logic" from the router code into its own modules, which are quite often called services. The name service originates from Domain driven design and was made popular by the Spring framework.

Let's create a src/services directory and place the diaryService.ts file in it. The file contains two functions for fetching and saving diary entries:

import diaryData from '../../data/diaries.json'

const getEntries = () => {
  return diaryData;
};

const addEntry = () => {
  return null;
};

export default {
  getEntries,
  addEntry
};

But something is not right

17c

The hint says we might want to use resolveJsonModule. Let's add it to our tsconfig:

{
  "compilerOptions": {
    "target": "ES6",
    "outDir": "./build/",
    "module": "commonjs",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true,
    "resolveJsonModule": true  }
}

And our problem is solved.

NB: For some reason VSCode tends to complain it cannot find the file ../../data/diaries.json from the service despite the file existing. That is a bug in the editor, and goes away when the editor is restarted.

Earlier we saw how the compiler can decide the type of a variable by the value it is assigned. Similarly the compiler can interpret large data sets consisting of objects and arrays. Due to this the compiler can actually warn us if we try to do something suspicious with the json data we are handling. For example if we are handling an array containing objects of specific type, and we try to add an object which does not have all the fields the other objects have, or has type conflicts (for example a number where there should be a string), the compiler can give us a warning.

Even though the compiler is pretty good at making sure we don't do anything unwanted, it is safer to define the types for the data ourselves.

Currently we have a basic working TypeScript express app, but there are barely any actual typings in the code. Since we know what type of data should be accepted for the weather and visibility fields, there is no reason for us not to include their types to the code.

Let's create a file for our types, types.ts, where we'll define all our types for this project.

First, let's type the Weather and Visibility values using a union type of the allowed strings:

export type Weather = 'sunny' | 'rainy' | 'cloudy' | 'windy' | 'stormy';

export type Visibility = 'great' | 'good' | 'ok' | 'poor';

And from there we can continue by creating a DiaryEntry type, which will be an interface:

export interface DiaryEntry {
  id: number;
  date: string;
  weather: Weather;
  visibility: Visibility;
  comment: string;
}

We can now try to type our imported json:

import diaryData from '../../data/diaries.json';

import { DiaryEntry } from '../types';
const diaries: Array<DiaryEntry> = diaryData;
const getEntries = (): Array<DiaryEntry> => {  return diaries;};

const addEntry = () => {
  return null;
};

export default {
  getEntries,
  addEntry
};

But since the json already has its values declared, assigning a type for the data set results in an error:

19b

The end of the error message reveals the problem: the weather fields are incompatible. In DiaryEntry we specified that its type is Weather, but TypeScript compiler had inferred its type to be string.

We can fix the problem by doing type assertion. This should be done only if we are certain we know what we are doing. If we assert the type of the variable diaryData to be DiaryEntry with the keyword as, everything should work:

import diaryData from '../../data/entries.json'

import { Weather, Visibility, DiaryEntry } from '../types'

const diaries: Array<DiaryEntry> = diaryData as Array<DiaryEntry>;
const getEntries = (): Array<DiaryEntry> => {
  return diaries;
}

const addEntry = () => {
  return null
}

export default {
  getEntries,
  addEntry
};

We should never use type assertion unless there is no other way to proceed, as there is always the danger we assert an unfit type to an object and cause a nasty runtime error. While the compiler trusts you to know what you are doing when using as, doing this we are not using the full power of TypeScript but relying on the coder to secure the code.

In our case we could change how we export our data so we can type it within the data file. Since we cannot use typings in a JSON-file, we should convert the json-file to a ts-file which exports the typed data like so:

import { DiaryEntry } from "../src/types";

const diaryEntries: Array<DiaryEntry> = [
  {
      "id": 1,
      "date": "2017-01-01",
      "weather": "rainy",
      "visibility": "poor",
      "comment": "Pretty scary flight, I'm glad I'm alive"
  },
  // ...
];

export default diaryEntries;

Now when we import the array, the compiler interprets it correctly and the weather and visibility fields are understood right:

import diaries from '../../data/diaries';
import { DiaryEntry } from '../types';

const getEntries = (): Array<DiaryEntry> => {
  return diaries;
}

const addEntry = () => {
  return null;
}

export default {
  getEntries,
  addEntry
};

Note, that if we want to be able to save entries without a certain field, e.g. comment, we could set the type of the field as optional by adding ? to the type declaration:

export interface DiaryEntry {
  id: number;
  date: string;
  weather: Weather;
  visibility: Visibility;
  comment?: string;
}

Node and JSON modules

It is important to take note of a problem that may arise when using the tsconfig resolveJsonModule option:

{
  "compilerOptions": {
    // ...
    "resolveJsonModule": true  }
}

According to the node documentation for file modules, node will try to resolve modules in order of extensions:

 ["js", "json", "node"]

In addition to that, by default, ts-node and ts-node-dev extend the list of possible node module extensions to:

 ["js", "json", "node", "ts", "tsx"]

NB: The validity of .js, .json and .node files as modules in Typescript depend on environment configuration, including tsconfig options such as allowJs and resolveJsonModule.

Consider a flat folder structure containing files:

  ├── myModule.json
  └── myModule.ts

In typescript, with the resolveJsonModule option set to true, the file myModule.json becomes a valid node module. Now, imagine a scenario where we wish to take the file myModule.ts into use:

import myModule from "./myModule";

Looking closely at the order of node module extensions:

 ["js", "json", "node", "ts", "tsx"]

We notice that the .json file extension takes precedence over .ts and so myModule.json will be imported and not myModule.ts.

In order to avoid time eating bugs, it is recommended that within a flat directory, each file with a valid node module extension has a unique filename.

Utility Types

Sometimes we might want to use a specific modification of a type. For example, consider a page for listing some data, some of which is sensitive and some of which is non-sensitive. We might want to be sure that no sensitive data is used or displayed. We could pick the fields of a type we allow to be used to enforce this. We can do that by using the utility type Pick.

In our project we should consider that Ilari might want to create a listing of all his diary entries excluding the comment field, since during a very scary flight he might end up writing something he wouldn't necessarily want to show anyone else.

The Pick utility type allows us to choose which fields of an existing type we want to use. Pick can be used to either construct a completely new type, or to inform a function what it should return on runtime. Utility types are a special kinds of type tools, but they can be used just like regular types.

In our case, in order to create a "censored" version of the DiaryEntry for public displays, we can use Pick in the function declaration:

const getNonSensitiveEntries =
  (): Array<Pick<DiaryEntry, 'id' | 'date' | 'weather' | 'visibility'>> => {
    // ...
  }

and the compiler would expect the function to return an array of values of the modified DiaryEntry type, which include only the four selected fields.

Since Pick requires the type it modifies to be given as a type variable, just like Array does, we now have two nested type variables and the syntax is starting to look a bit odd. We can improve the code's readability by using the alternative array syntax:

const getNonSensitiveEntries =
  (): Pick<DiaryEntry, 'id' | 'date' | 'weather' | 'visibility'>[] => {
    // ...
  }

In this case we want to exclude only one field, so even better would be to use the Omit utility type, which we can use to declare which fields to exclude:

const getNonSensitiveEntries = (): Omit<DiaryEntry, 'comment'>[] => {
  // ...
}

Another way would be to declare a completely new type for the NonSensitiveDiaryEntry:

export type NonSensitiveDiaryEntry = Omit<DiaryEntry, 'comment'>;

The code now becomes

import diaries from '../../data/diaries';
import { NonSensitiveDiaryEntry, DiaryEntry } from '../types';
const getEntries = (): DiaryEntry[] => {
  return diaries;
};

const getNonSensitiveEntries = (): NonSensitiveDiaryEntry[] => {  return diaries;
};

const addEntry = () => {
  return null;
};

export default {
  getEntries,
  addEntry,
  getNonSensitiveEntries};

One thing in our application is a cause for concern. In the getNonSensitiveEntries we are returning the complete diary entries, and no error is given despite typing!

This happens because TypeScript only checks whether we have all of the required fields or not, but excess fields are not prohibited. In our case this means that it is not prohibited to return an object of type DiaryEntry[], but if we were to try to access the comment field, it would not be possible because we would be accessing a field that TypeScript is unaware of even though it exists.

Unfortunately this can lead to unwanted behaviour if you are not aware of what you are doing; the situation is valid as long as TypeScript is concerned, but you are most likely allowing use that is not wanted. If we were now to return all of the diaryEntries from the getNonSensitiveEntries function to the frontend, we would actually be leaking the unwanted fields to the requesting browser even thought our types seem to imply otherwise!

Because TypeScript doesn't modify the actual data but only its type, we need to exclude the fields ourselves:

import diaries from '../../data/entries.js'

import { NonSensitiveDiaryEntry, DiaryEntry } from '../types'

const getEntries = () : DiaryEntry[] => {
  return diaries
}

const getNonSensitiveEntries = (): NonSensitiveDiaryEntry [] => {  return diaries.map(({ id, date, weather, visibility }) => ({    id,    date,    weather,    visibility,  }));};
const addDiary = () => {
  return []
}

export default {
  getEntries,
  getNonSensitiveEntries,
  addDiary
}

If we now would try to return this data with the basic DiaryEntry type, i.e. if we would type the function as follows

const getNonSensitiveEntries = () : DiaryEntry[] => {

we would get the following error:

22b

Again the last line of the error message is the most helpful one. Let's undo this undesired modification.

Utility types include many handy tools, and it is definitely worth it to take some time to study the documentation.

Finally, we can complete the route which returns all diary entries:

import express from 'express';
import diaryService from '../services/diaryService';
const router = express.Router();

router.get('/', (_req, res) => {
  res.send(diaryService.getNonSensitiveEntries());});

router.post('/', (_req, res) => {
    res.send('Saving a diary!');
});

export default router;

The response is what we expect it to be

26

Preventing an accidental undefined result

Let's extend the backend to support fetching one specific entry with a HTTP GET request to route api/diaries/:id.

The DiaryService needs to be extended with findById-function:

// ...

const findById = (id: number): DiaryEntry => {  const entry = diaries.find(d => d.id === id);  return entry;};
export default {
  getEntries,
  getNonSensitiveEntries,
  addDiary,
  findById}

But once again, a new problem emerges:

23e

The issue is, that there is no guarantee that an entry with the specified id can be found. It is good that we are made aware of this potential problem already at compile phase. Without TypeScript we would not be warned about this problem, and in the worst case scenario we could have ended up returning an undefined object instead of informing the user about the specified entry not being found.

First of all in cases like this we need to decide what the return value should be if an object is not found, and how the case should be handled. The find method of an array returns undefined if the object is not found, and this is actually fine with us. We can solve our problem by typing the return value as follows

const findById = (id: number): DiaryEntry | undefined => {  const entry = diaries.find(d => d.id === id);
  return entry;
}

The route handler is the following

import express from 'express';
import diaryService from '../services/diaryService'

router.get('/:id', (req, res) => {
  const diary = diaryService.findById(Number(req.params.id));

  if (diary) {
    res.send(diary);
  } else {
    res.sendStatus(404);
  }
})

// ...

export default router;

Adding a new diary

Let's start building the HTTP POST endpoint for adding new flight diary entries. The new entries should have the same type as the existing data.

The code handling of the response looks as follows

router.post('/', (req, res) => {
  const { date, weather, visibility, comment } = req.body;
  const newDiaryEntry = diaryService.AddEntry(
    date,
    weather,
    visibility,
    comment,
  );
  res.json(newDiaryEntry);
});

corresponding method in diaryService looks like this

import {
  NonSensitiveDiaryEntry, DiaryEntry,
  Visibility, Weather  } from '../types';


const addEntry = (
    date: string, weather: Weather, visibility: Visibility, comment: string
  ): DiaryEntry => {

  const newDiaryEntry = {
    id: Math.max(...diaries.map(d => d.id)) + 1,
    date,
    weather,
    visibility,
    comment,
  }

  diaries.push(newDiaryEntry);
  return newDiaryEntry;
}

As you can see, the addDiary function is becoming quite hard to read now that we have all the fields as separate parameters. It might be better to just send the data as an object to the function:

router.post('/', (req, res) => {
  const { date, weather, visibility, comment } = req.body;
  const newDiaryEntry = diaryService.addDiary({    date,
    weather,
    visibility,
    comment,
  });  res.json(newDiaryEntry);
})

But wait, what is the type of this object? It is not exactly a DiaryEntry, since it is still missing the id field. It could be useful to create a new type, NewDiaryEntry, for a not yet saved entry. Let's create that in types.ts using the existing DiaryEntry type and the Omit utility type:

export type NewDiaryEntry = Omit<DiaryEntry, 'id'>;

Now we can use the new type in our DiaryService, and destructure the new entry object when creating an entry to be saved:

import { NewDiaryEntry, NonSensitiveDiaryEntry, DiaryEntry } from '../types';
// ...

const addDiary = ( entry: NewDiaryEntry ): DiaryEntry => {  const newDiaryEntry = {
    id: Math.max(...diaries.map(d => d.id)) + 1,
    ...entry  };

  diaries.push(newDiaryEntry);
  return newDiaryEntry;
};

Now the code looks much cleaner!

In order to parse the incoming data we must have the json middleware configured:

import express from 'express';
import diaryRouter from './routes/diaries';
const app = express();
app.use(express.json());
const PORT = 3000;

app.use('/api/diaries', diaryRouter);

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Now the application is ready to receive HTTP POST requests for new diary entries of the correct type!

Proofing requests

There are plenty of things which can go wrong when we accept data from outside sources. Applications rarely work completely on their own, and we are forced to live with the fact that data from sources outside of our system cannot be fully trusted. When we receive data from an outside source, there is no way it can be already typed when we receive it. We need to make decisions on how to handle the uncertainty that comes with this.

Express handles parsing the request body by asserting the type any to all the body's fields. In our case this is not apparent in the editor at all, but if we start looking at the variables more closely and hover over any of them, we can see that each of them is indeed type any. The editor does not complain in any way when we pass this data to the addDiary function as parameters:

27

The value of type any can be assigned to any type of variable since it might be the wanted type. It's definitely not safe to trust this, so always check the incoming values (regardless of whether we are using TypeScript or not).

We could just add simple exists and is-value-valid checks to the function defining the route, it is better to write the parsing and validation logic to a separate file utils.ts.

We need to define a function toNewDiaryEntry that receives the request body as a parameter and returns a properly typed NewDiaryEntry object. The route definition uses the function as follows

import toNewDiaryEntry from '../utils';
// ...

router.post('/', (req, res) => {
  try {
    const newDiaryEntry = toNewDiaryEntry(req.body);
    const addedEntry = diaryService.addDiary(newDiaryEntry);    res.json(addedEntry);
  } catch (e) {
    res.status(400).send(e.message);
  }
})

Since we are now making secure code and trying to ensure that we are getting exactly the data we want from the requests, we should get started with parsing and validating each field we are expecting to receive.

The skeleton of the function toNewDiaryEntry looks like the following:

import { NewDiaryEntry } from './types';

const toNewDiaryEntry = (object): NewDiaryEntry => {
  const newEntry: NewDiaryEntry = {
    // ...
  }

  return newEntry;
}

export default toNewDiaryEntry;

The function should parse each field and make sure that the return value is exactly of type NewDiaryEntry. This means we should check each field separately.

Once again we have a type issue: what is the object type? Since the object is in fact the body of a request, Express has typed it as any. Since the idea of this function is to map fields of unknown type to fields of the correct type and check whether they are defined as expected, this might be the rare case where we actually want to allow the any type.

However if we type the object as any, eslint gives us a complaint:

24e

This is due to the eslint-rule no-explicit-any which prevents us from explicitly setting type to be any. In general this is a good rule, and undesired just in this particular file. We can allow using any in this file by disabling the eslint-rule in the file. This happens by adding the following line to the file:

/* eslint-disable @typescript-eslint/no-explicit-any */

Let us start creating the parsers for each of the fields of object.

To validate the comment field we need to check that it exists, and to ensure that it is of the type string.

The function should look something like this:

const parseComment = (comment: any): string => {
  if (!comment || !isString(comment)) {
    throw new Error('Incorrect or missing comment: ' + comment);
  }

  return comment;
}

The function gets a parameter of type any and returns it as type string if it exists and is of the right type.

The string validation function looks like this

const isString = (text: any): text is string => {
  return typeof text === 'string' || text instanceof String;
};

The function is a so called type guard. That means it is a function which returns a boolean and which has a type predicate as the return type. In our case the type predicate is

text is string

The general form of a type predicate is parameterName is Type where the parameterName is the name of the function parameter and Type is the targeted type.

If the type guard function returns true, the TypeScript compiler knows that the tested variable has the type that was defined in the type predicate.

Before the type guard is called, the actual type of the variable comment is not known:

28e

But after the call, if the code proceeds past the exception (that is the type guard returned true), compiler knows that comment is of the type string:

29e

Why do we have two conditions in the string type guard?

const isString = (text: any): text is string => {
  return typeof text === 'string' || text instanceof String;}

would it not be enough to write the guard like this

const isString = (text: any): text is string => {
  return typeof text === 'string';
}

Most likely the simpler form is good enough for all practical purposes. However, if we want to be absolutely sure, both conditions are needed. There are two different ways to create string objects in JavaScript which both work a bit differently with respect to the typeof and instanceof operators:

const a = "I'm a string primitive";
const b = new String("I'm a String Object");
typeof a; --> returns 'string'
typeof b; --> returns 'object'
a instanceof String; --> returns false
b instanceof String; --> returns true

However, it is unlikely that anyone would create a string with a constructor function. Most likely the simpler version of the type guard would be just fine.

Next, let's consider the date field. Parsing and validating the date object is pretty similar to what we did with comments. Since TypeScript doesn't really know a type for a date, we need to treat it as a string. We should however still use JavaScript level validation to check whether the date format is acceptable.

We will add the following functions

const isDate = (date: string): boolean => {
  return Boolean(Date.parse(date));
};

const parseDate = (date: any): string => {
  if (!date || !isString(date) || !isDate(date)) {
      throw new Error('Incorrect or missing date: ' + date);
  }
  return date;
};

The code is really nothing special. The only thing is, that we can't use a type guard here since a date in this case is only considered to be a string. Note, that even though the parseDate function accepts the date variable as any, after we check the type with isString its type is set as string, which is why we can give the variable to the isDate function requiring a string without any problems.

Finally we are ready to move on to the last two types, Weather and Visibility.

We would like the validation and parsing to work as follows:

const parseWeather = (weather: any): Weather => {
  if (!weather || !isString(weather) || !isWeather(weather)) {
      throw new Error('Incorrect or missing weather: ' + weather)
  }
  return weather;
};

The question is, how can we validate that the string is of a specific form? One possible way to write the type guard would be this:

const isWeather = (str: any): str is Weather => {
  return ['sunny', 'rainy', 'cloudy', 'stormy' ].includes(str);
};

This would work just fine, but the problem is that the list of possible weathers does not necessarily stay in sync with the type definitions if the type is altered. This is most certainly not good, since we would like to have just one source for all possible weather types.

In our case a better solution would be to improve the actual Weather type. Instead of a type alias we should use the TypeScript enum, which allows us to use the actual values in our code in runtime, not only in the compilation phase.

Let us redefine the type Weather as follows:

export enum Weather {
  Sunny = 'sunny',
  Rainy = 'rainy',
  Cloudy = 'cloudy',
  Stormy = 'stormy',
  Windy = 'windy',
}

Now we can check that a string is one of the accepted values, and the type guard can be written like this:

const isWeather = (param: any): param is Weather => {
  return Object.values(Weather).includes(param);
};

One thing to notice here is that we have changed the parameter type to any. If it would be string, the includes check would not compile. This makes sense also if you consider the reusability of the function. Allowing any as a parameter, the function can be used with confidence knowing that whatever we might feed to it, the function always tells us whether the variable is a valid weather or not.

The function parseWeather can be simplified a bit

const parseWeather = (weather: any): Weather => {
  if (!weather || !isWeather(weather)) {      throw new Error('Incorrect or missing weather: ' + weather);
  }
  return weather;
};

One issue arises after these changes. Our data does not conform to our types anymore:

30

This is because we cannot just assume a string is an enum.

We can fix this by mapping the initial data elements to DiaryEntry type with the toNewDiaryEntry function:

import { DiaryEntry } from "../src/types";
import toNewDiaryEntry from "../src/utils";

const data = [
  {
      "id": 1,
      "date": "2017-01-01",
      "weather": "rainy",
      "visibility": "poor",
      "comment": "Pretty scary flight, I'm glad I'm alive"
  },
  // ...
]

const diaryEntries: DiaryEntry [] = data.map(obj => {
  const object = toNewDiaryEntry(obj) as DiaryEntry
  object.id = obj.id
  return object
})

export default diaryEntries

Note that since toNewDiaryEntry returns an object of the type NewDiaryEntry we need to assert it to be DiaryEntry with the as operator.

Enums are usually used when there is a set of predetermined values which are not expected to change in the future. Usually enums are used for much tighter unchanging values (for example weekdays, months, directions) but since they offer us a great way to validate our incoming values we might as well use them in our case.

We still need to give the same treatment to visibility. The enum looks following

export enum Visibility {
  Great = 'great',
  Good = 'good',
  Ok = 'ok',
  Poor = 'poor',
}

The type guard and the parser are below

const isVisibility = (param: any): param is Visibility => {
  return Object.values(Visibility).includes(param);
};

const parseVisibility = (visibility: any): Visibility => {
  if (!visibility || !isVisibility(visibility)) {
      throw new Error('Incorrect or missing visibility: ' + visibility);
  }
  return visibility;
};

And finally we can finalize the toNewDiaryEntry function that takes care of validating and parsing the fields of the post data:

const toNewDiaryEntry = (object: any): NewDiaryEntry => {
  return {
    date: parseDate(object.date),
    comment: parseComment(object.comment),
    weather: parseWeather(object.weather),
    visibility: parseVisibility(object.visibility)
  };
};

The first version of our flight diary application is now completed!

If we now try to create a new diary entry with invalid or missing fields we are getting an appropriate error message

30b