When Koa meets Typescript

   Recent restructuring of the Operations Side Mid-stage project, the current selection is koa2+typescript. In the actual production, we really understand typescript Benefits of type.

To illustrate the advantages of typescript more visually, let's start with a scenario:
BUG site
As a particularly flexible language, the disadvantage is that in complex logic writing, data structure information may be lost due to logic complexity, changes in personnel, and so on, resulting in implicit errors in the code written.

This time, for example, I was writing a node script for my blog.

const result = [];

function findAllFiles(root) {
  const files = fs.readdirSync(root);
  files.forEach(name => {
    const file = path.resolve(root, name);
    if (isFolder(file)) {
    } else {
        path: file,
        check: false,
        content: fs.readFileSync(file)

result stores path, check, content information for all files that are recursively traversed, where the content information is passed to the check(content: string, options: object) method of prettier.js.

Obviously, the above code is incorrect, but it is extremely difficult to discover. Only when it is run can it be located by a stack error. But with ts, errors can be discovered immediately and the code is robust.

Let's put this problem at the end of the article and see how ts can be used in the koa project.
Project Directory
The architecture of the whole project is refreshing because there is no historical burden. As follows:

├── README.md
├── bin # Script file to store scripts
├── dist # Compile packaged js files
├── docs # Detailed Documentation
├── package.json # npm
├── sh # Scripts such as pm2
├── src # Project Source
├── tmp # Place to store temporary files
└── tsconfig.json # typescript compilation configuration

typescript compilation and npm configuration

Because ts is used to write code, you need to write a configuration file specifically for typescript: tsconfig.json. Depending on your personal habits and the ts items in the previous group, configure as follows:

  "compilerOptions": {
    "module": "commonjs", // Compile generated module system code
    "target": "es2017", // Specify the target version of ecmascript
    "noImplicitAny": true, // Prohibit implicit any type
    "outDir": "./dist",
    "sourceMap": false,
    "allowJs": false, // Is js allowed
    "newLine": "LF"
  "include": ["src/**/*"]

For some projects with a legacy of history, or projects that are progressively reconstructed from js to ts, allowJs should be true here and noImplicitAny false because of the large amount of js legacy code.

In package.json, configure two scripts, one in dev mode and the other in prod mode:

  "scripts": {
    "dev": "tsc --watch & export NODE_ENV=development && node bin/dev.js -t dist/ -e dist/app.js",
    "build": "rm -rf dist/* && tsc"

In dev mode, tsc is required to listen for changes to the ts file specified in the include in the configuration and compile in real time. bin/dev.js is a monitoring script written to suit the needs of the project. It listens for compiled JS files in the dist/directory and restarts the server once the restart condition is met.

Type declaration file

Type declarations for koajs and common plug-ins are installed under @types:

npm i --save-dev @types/koa @types/koa-router @types/koa2-cors @types/koa-bodyparser

Distinguish dev/prod environments

For future development and online convenience, the src/config/directory is as follows:

├── dev.ts
├── index.ts
└── prod.ts

Configuration is divided into prod and dev. In dev mode, print information to the console; Under prod, log information needs to be written to the specified location. Similarly, no authentication is required under dev, and Intranet authentication is required under prod. Therefore, the extends feature of ts is used to reuse data declarations:

// mode: dev
export interface ConfigScheme {
  // Listening Port
  port: number;
  // mongodb configuration
  mongodb: {
    host: string;
    port: number;
    db: string;
// mode: prod
export interface ProdConfigScheme extends ConfigScheme {
  // Log storage location
  logRoot: string;

In index.ts, through process.env.NODE_ The ENV variable value determines the pattern and then exports the corresponding configuration.

import { devConf } from "./dev";
import { prodConf } from "./prod";

const config = process.env.NODE_ENV === "development" ? devConf : prodConf;

export default config;

In this way, the outside world can be introduced directly. However, during development, such as authentication middleware. Although dev mode is not turned on, the config type introduced when writing it is ConfigScheme, and the ts compiler will error when accessing fields on ProdConfigScheme s.

At this point, ts's assertion comes in handy:

import config, { ProdConfigScheme } from "./../config/";

const { logRoot } = config as ProdConfigScheme;

Middleware Writing
For the whole project, the business logic associated with koa is mainly reflected in the middleware. This paper takes the writing of Operational Retention Middleware which is necessary for operating systems as an example to show how to write business logic and data logic of Middleware in ts.

Introduce koa and the written wheels:

import * as Koa from "koa";
import { print } from "./../helpers/log";
import config from "./../config/";
import { getDB } from "./../database/mongodb";

const { mongodb: mongoConf } = config; // mongo configuration
const collectionName = "logs"; // Collection Name

The data fields that need to be retained in operation retention are:

staffName: Operator
visitTime: Operation time
url: Interface Address
params: All parameters passed from the front end

ts directly constrains the field type with the help of interface. At a glance, future maintainers will understand the data structure with which we interact with db without the aid of documentation.

interface LogScheme {
  staffName: string;
  visitTime: string;
  url: string;
  params?: any;

Finally, you write the middleware function logic where the parameters need to specify the type. Of course, it's okay to directly indicate that the parameter is any type, but that's no different from js, and you don't realize the benefits of ts for documented programming.

Because @types/koa has been installed before, we do not need to write the.d.ts file manually here. Moreover, Koa's built-in data types have been suspended from Koa import ed earlier (yes, TS has done a lot for us). The type of context is Koa.BaseContext, and the type of callback function is () => Promise

async function logger(ctx: Koa.BaseContext, next: () => Promise<any>) {
  const db = await getDB(mongoConf.db); // Get Link Instances from the db Link Pool
  if (!db) {
    ctx.body = "mongodb errror at controllers/logger";
    ctx.status = 500;

  const doc: LogScheme = {
    staffName: ctx.headers["staffname"] || "unknown",
    visitTime: Date.now().toString(10),
    url: ctx.url,
    params: ctx.request.body

  // There is no need for await to wait for this logic to complete
    .catch(error =>
      print(`fail to log info to mongo: ${error.message}`, "error")

  return next();

export default logger;

Unit function

Take a unit function of log output as an example to illustrate the application of Index Signature.

First, the log level is constrained by the union type:

type LogLevel = "log" | "info" | "warning" | "error" | "success";

At this point, you plan to prepare a mapping: the log level=>the data structure of the file name, such as the info level log output file is info.log. Obviously, all key s of this object must conform to LogLevel. Write as follows:

const localLogFile: {
  [level in LogLevel]: string | void;
} = {
  log: "info.log",
  info: "info.log",
  warning: "warning.log",
  error: "error.log",
  success: "success.log"

For log-level logs, you don't need to output to a file; you just need to print to the console. Then the localLogFile should not have a log field. If the log field is removed directly, the ts compiler will make the following error:

Property 'log' is missing in type '{ info: string; warning: string; error: string; success: string; }' but required in type '{ log: string | void; info: string | void; warning: string | void; error: string | void; success: string | void; }'.

Based on the error, set the index signature field to Optional here:

const localLogFile: {
  [level in LogLevel]?: string | void;
} = {
  info: "info.log",
  warning: "warning.log",
  error: "error.log",
  success: "success.log"

About export

When export is used to export complex objects, add a type declaration and do not rely on type inference from ts.


import level0 from "./level0";

export interface ApiScheme {
  method: ApiMethod;
  host: string;

export interface ApiSet {
  [propName: string]: ApiScheme;

export const apis: ApiSet = {


import { ApiSet } from "./index";

// Declare the data type of the exported object
export const level0: ApiSet = {
  "qcloud.tcb.getPackageInfo": {
    method: "post",
    host: tcb.dataUrl

  "qcloud.tcb.getAlarmRecord": {
    method: "post",
    host: tcb.dataUrl

Back to the beginning

Back in the beginning scenario, if typescript is used, we will first declare the format of each object in the result:

interface FileInfo {
  path: string;
  check: boolean;
  content: string;

const result: FileInfo[] = [];

At this point, you will find that the typescript compiler has already given an error, in the line content: fs.readFileSync(file), the error message is as follows:

You cannot type " Buffer"Assign to Type string". 

Instead of writing hundreds of lines, and then running, locating the problem based on one line of stack errors.

Think about it carefully, if it's a large 30-person node/front end project, how high is the risk of error? How high is the cost of positioning errors? So just want to say ts is great!

Tags: node.js TypeScript Back-end koa

Posted on Thu, 18 Nov 2021 12:47:29 -0500 by jredwilli