Recently, I am reconstructing the mid-stage project on the operation side, and the current selection is KOA2 +typescript. In the real world, the benefits of typescript types are realized.

This post is from When Koa Meets Typescript by Xintan Blog, and more posts have been posted on Github

Welcome to exchange and Star

To illustrate typescript’s strengths, let’s start with a scenario:

At the scene of the BUG

As a highly flexible language, the disadvantage is that in the process of writing complex logic, data structure information may be lost due to the complexity of logic, personnel changes, etc., so that the code written contains hidden errors.

For example, I encountered this situation while scripting node 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)) {
      findAllFiles(file);
    } else {
      result.push({
        path: file,
        check: false.content: fs.readFileSync(file) }); }}); }Copy the code

Result saves the path, check, and content of the recursively traversed files, where content is passed to the check(Content: String, options: Object) method for prettier.js.

Obviously, there are bugs in the code above, but they are extremely difficult to spot. Only when it is running can it be located by a stack error. But with TS, you can find errors immediately and keep your code robust.

This question will be left at the end of the article. Let’s look at the application of TS in the KOA project.

Project directory

With no historical baggage, the architecture of the project is very clean. As follows:

.Bass Exercises ── Readme.md Bass Exercises ─ bin# save the script file for scripts├ ─ ─ distCompile the packaged JS file├ ─ ─ the docs# Detailed documentation├ ─ ─ package. Json# npm├ ─ ─ sh# pm2, etc├ ─ ─ the SRC# project source code├ ─ ─ TMP# place to store temporary files└ ─ ─ tsconfig. Json# typescript compile configuration
Copy the code

Typescript compilation and NPM configuration

Because you write your code in TS, you need to write a typescript configuration file: tsconfig.json. Based on personal habits and previous TS projects in the group, the configuration is as follows:

{"compilerOptions": {"module": "commonjs", // Target "es2017", // specify ecMAScript target" noImplicitAny": True, // disallow implicit any types "outDir": "./dist", "sourceMap": false, "allowJs": false, // allowJs" newLine": "LF"}, "include": ["src/**/*"] }Copy the code

AllowJs should be true and noImplicitAny should be false for legacy projects, or for projects that progressively refactor from JS to TS, because there is a lot of LEGACY JS 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"}}Copy the code

In dev mode, TSC is required to listen for changes to ts files specified in the include configuration and compile them in real time. Bin /dev.js is a listening script written according to the needs of the project. It will listen to the compiled JS files in the dist/ directory and restart the server once the restart conditions are met.

Type declaration file

Koajs and common plug-in type declarations are installed under @types:

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

Distinguish between dev/prod environments

In order to facilitate future development and online, the SRC /config/ directory is as follows:

.├ ─ dev.School Exercises ─ indexCopy the code

The configuration is divided into prod and dev. In dev mode, information is printed to the console. Under PROD, log information needs to be written to the specified location. Similarly, dev does not require authentication, while PROD requires Intranet authentication. Therefore, the extends property of TS is used to reuse data declarations:

// mode: dev
export interface ConfigScheme {
  // Listen on the port
  port: number;
  / / mongo configuration
  mongodb: {
    host: string;
    port: number;
    db: string;
  };
}
// mode: prod
export interface ProdConfigScheme extends ConfigScheme {
  // Log storage location
  logRoot: string;
}
Copy the code

In index.ts, the mode is determined by the value of the process.env.node_env variable and the corresponding configuration is exported.

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

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

export default config;
Copy the code

In this way, the outside world can be directly introduced. But during development, for example, authentication middleware. Although this is not enabled in dev mode, it is written with the config type ConfigScheme introduced, and the TS compiler will report an error when accessing fields on ProdConfigScheme.

This is where TS’s assertion comes in handy:

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

const { logRoot } = config as ProdConfigScheme;
Copy the code

Middleware writing

For the overall project, the business logic associated with koA is mainly embodied in the middleware. Here to the operation system must “operation retention middleware” as an example to show how to write middleware business logic and data logic in TS.

Introducing koA and 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"; // Set name
Copy the code

Data fields to be retained in operation retention are:

StaffName: operator visitTime: operation time URL: interface address params: all parameters sent from the front endCopy the code

Ts uses interface to directly constrain field types. At a glance, for future maintainers, there is little need for documentation to understand the data structure with which we interact with db.

interface LogScheme {
  staffName: string;
  visitTime: string;
  url: string; params? :any;
}
Copy the code

Finally, write middleware function logic, parameters need to specify the type. Of course, it is possible to specify that the argument is of type any, but this is no different from JS and does not benefit from ts’s documented programming.

Since @types/koa was installed earlier, we don’t need to write the.d.ts file manually here. Also, koA’s built-in data types are already attached to the KOA imported earlier (yes, TS does a lot of work for us). The context is of type koa.basecontext, and the callback is of type () => Promise

async function logger(ctx: Koa.BaseContext, next: () => Promise<any>) { const db = await getDB(mongoConf.db); // Get link instance from db link pool if (! db) { ctx.body = "mongodb errror at controllers/logger"; ctx.status = 500; return; } const doc: LogScheme = { staffName: ctx.headers["staffname"] || "unknown", visitTime: Date.now().toString(10), url: ctx.url, params: ctx.request.body }; Db.collection (collectionName).insertone (doc). Catch (error => print(' fail to log info to mongo: ${error.message}`, "error") ); return next(); } export default logger;Copy the code

Cell function

Here is a log output unit function as an example, the application of “index signature”.

First, log levels are constrained by union types:

type LogLevel = "log" | "info" | "warning" | "error" | "success";
Copy the code

In this case, you want to prepare a mapping: log level => data structure of file name, for example, the output file of info level logs is info.log. Obviously, all keys of this object must conform to LogLevel. It is written as follows:

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

For log-level logs, you don’t need to export them to a file, just print them to the console. LocalLogFile should not have a log field. If the localLogFile does not have a log field, the ts compiler will report 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; } '.
Copy the code

Based on the error, set the index signature field to “optional” :

const localLogFile: {
  [level inLogLevel]? :string | void;
} = {
  info: "info.log",
  warning: "warning.log",
  error: "error.log",
  success: "success.log"
};
Copy the code

About the export

When exporting complex objects using export, add the type declaration and do not rely on type inference with TS.

Index. Ts:

import level0 from "./level0";

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

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

export constapis: ApiSet = { ... level0 };Copy the code

level0.ts:

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
  }
};
Copy the code

Back to the beginning

Going back to the opening scenario, in typescript we declare the format of each object in result:

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

const result: FileInfo[] = [];
Copy the code

At this point, you’ll notice that the typescript compiler is already reporting an error in the content: fs.readfilesync (file) line:

Cannot assign type "Buffer" to type "string".Copy the code

This way, errors can be found immediately as the code is written. You don’t write hundreds of lines and then run around and try to locate the problem according to the stack error line by line.

Think about it, how high is the risk of something going wrong in a large Node/front-end project with 30 people working together? What is the cost of mislocation? So, just to say ts smells good!

Reference books

  • TypeScript Introduction
  • TypeScript Deep Dive