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