» Node.js创建命令行程序grep » 2. 开发 » 2.4 添加类型支持

添加类型支持

你可以用 TypeScript 引入静态类型。TypeScript 是 JavaScript 的一个超集,包含静态类型,并且可以与 Node.js 项目一起使用,在开发过程中可以及时捕获与类型相关的错误。

如果尚未安装,可将 TypeScript 作为项目的开发依赖项安装:

npm install --save-dev typescript

运行以下命令生成 tsconfig.json 文件,这是 TypeScript 的配置文件:

# tsc 代表 "TypeScript compiler"
npx tsc --init

npx 代表 "Node Package eXecute"。它是随 npm 一起提供的命令行工具,用于执行 Node.js 包。
npx 使得从 npm 包中运行二进制文件变得方便,无需全局安装即可执行。

tsconfig.json:

{
  "compilerOptions": {
    "target": "es2017",                                  /* 设置用于生成的 JavaScript 的 JavaScript 语言版本,并包括兼容的库声明。 */
    "module": "ESNext",                                  /* 指定生成的模块代码。 */
    "rootDir": "./",                                     /* 指定源文件的根文件夹。 */
    "outDir": "./dist",                                  /* 指定输出文件夹。 */
    "esModuleInterop": true,                             /* 生成额外的 JavaScript 以便支持导入 CommonJS 模块。 */
    "forceConsistentCasingInFileNames": true,            /* 确保导入的大小写正确。 */
    /* 类型检查 */
    "strict": true,                                      /* 启用所有严格的类型检查选项。 */
    "skipLibCheck": true                                 /* 跳过对所有 .d.ts 文件的类型检查。 */
  },
  "include": [
    "bin/*.ts",
    "lib/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

更新 package.json:

...
  "type": "module",
  "scripts": {
...

这用于指示 JavaScript 文件是 ES6 模块。即意味着文件使用 ECMAScript 模块语法(importexport 语句),而不是 CommonJS 语法(requiremodule.exports)。

TypeScript 使用类型定义文件(.d.ts)为库和模块提供类型信息。安装 Node.js 和 yargs 类型定义:

npm install --save-dev @types/node
npm install --save-dev @types/yargs

使用 TypeScript 编译器 (tsc) 将 TypeScript 代码编译为 JavaScript。运行以下命令:

npx tsc

这将在指定的输出目录中生成 JavaScript 文件。

lib/grep.ts:57:46 - error TS18046: 'error' is of type 'unknown'.

57     console.error("Error reading the file:", error.message);
                                                ~~~~~


Found 18 errors in the same file, starting at: lib/grep.ts:4

tsc 会报告出代码的许多类型错误,请尝试修复它们。

.js 拓展名改成 .ts,修改 lib/grep.ts

@@ -1,11 +1,24 @@
-const fs = require("fs");
-const path = require("path");
 
-async function grep(pattern, filePath, options = {}) {
+import fs from 'fs';
+import path from 'path';
+
+type Options  = {
+  ignoreCase: boolean;
+  invertMatch: boolean;
+}
+
+type MatchItem = [number, string];
+
+export type MatchResult = {
+  [key: string]: MatchItem[];
+}
+
+export async function grep(pattern: string, filePath: string, options: Options) {
   const { ignoreCase, invertMatch } = options;
   const lines = await _readFileLines(filePath);
   const regexFlags = ignoreCase ? "gi" : "g";
   const regex = new RegExp(pattern, regexFlags);
+  let matchingLines: MatchItem[]
   if (invertMatch) {
     matchingLines = _filterLines(regex, lines, false);
   } else {
@@ -14,7 +27,7 @@ async function grep(pattern, filePath, options = {}) {
   return { [filePath]: matchingLines };
 }
 
-async function grepRecursive(pattern, dirPath, options = {}) {
+export async function grepRecursive(pattern: string, dirPath: string, options: Options) {
   let results = {};
   try {
     const files = await fs.promises.readdir(dirPath);
@@ -32,35 +45,26 @@ async function grepRecursive(pattern, dirPath, options = {}) {
   return results;
 }
 
-function grepCount(result) {
+export function grepCount(result: MatchResult) {
   return Object.values(result).reduce(
     (count, lines) => count + lines.length,
     0
   );
 }
 -function _filterLines(regexPattern, lines, flag) {
-  return lines
-    .map((line, lineNumber) => {
-      const match = regexPattern.test(line);
-      return flag === match ? [lineNumber + 1, line.trim()] : null;
-    })
-    .filter(Boolean);
+function _filterLines(regexPattern: RegExp, lines: string[], flag: boolean): MatchItem[] {
+  const candidates: MatchItem[] = lines.map((line, index) => [index + 1, line.trim()]);
+  return candidates
+    .filter(([_, line]) => regexPattern.test(line) === flag);
 }
 
-async function _readFileLines(filePath) {
+async function _readFileLines(filePath: string) {
   try {
     // Read the file asynchronously
     const data = await fs.promises.readFile(filePath, "utf8");
     return data.split("\n");
-  } catch (error) {
+  } catch (error: any) {
     console.error("Error reading the file:", error.message);
   }
   return [];
 }
-
-module.exports = {
-  grep,
-  grepCount,
-  grepRecursive,
-};

MatchResults 是类型 { [key: string]: MatchItem[]; } 的别名,用于表示如下内容:

{
  'lib/grep.ts': [
    [ 31, 'let results = {};' ],
    [ 37, 'const result = !isSubDir' ],
    [ 40, 'results = { ...results, ...result };' ],
    [ 45, 'return results;' ],
    [ 48, 'export function grepCount(result: MatchResult) {' ]
  ]
}

bin/grepjs 更改为 bin/cmd.ts,并修改其代码:

@@ -1,51 +1,56 @@
 #!/usr/bin/env node
 
-const fs = require("fs");
-const yargs = require("yargs");
+import yargs from "yargs";
 
-const { grep, grepCount, grepRecursive } = require("../lib/grep");
+import { grep, grepCount, grepRecursive, MatchResult } from "../lib/grep.js";
 
 // Parse command-line options
-yargs.locale("en");
-const argv = yargs
+const argv = await yargs(process.argv.slice(2))
+  .locale('en')
   .usage("Usage: $0 [options] <pattern> <file>")
   .option("c", {
     alias: "count",
     describe: "Only a count of selected lines is written to standard output.",
     type: "boolean",
+    default: false,
   })
   .option("h", {
     alias: "help",
     describe: "Print a brief help message.",
     type: "boolean",
+    default: false,
   })
   .option("i", {
     alias: "ignore-case",
     describe:
       "Perform case insensitive matching. By default, it is case sensitive.",
     type: "boolean",
+    default: false,
   })
   .option("n", {
     alias: "line-number",
     describe:
       "Each output line is preceded by its relative line number in the file, starting at line 1. The line number counter is reset for each file processed. This option is ignored if -c is specified.",
     type: "boolean",
+    default: false,
   })
   .option("r", {
     alias: "recursive",
     describe: "Recursively search subdirectories listed.",
     type: "boolean",
+    default: false,
   })
   .option("v", {
     alias: "invert-match",
     describe:
       "Selected lines are those not matching any of the specified patterns.",
     type: "boolean",
+    default: false,
   })
   .demandCommand(2, "Please provide both pattern and file arguments.").argv;
 
-const pattern = argv._[0];
-const filePath = argv._[1];
+const pattern = argv._[0] as string;
+const filePath = argv._[1] as string;
 
 if (argv.help) {
   // Print help message and exit
@@ -54,8 +59,8 @@ if (argv.help) {
 }
 
 const options = {
-  ignoreCase: argv["ignore-case"],
-  invertMatch: argv["invert-match"],
+  ignoreCase: argv["ignore-case"] as boolean,
+  invertMatch: argv["invert-match"] as boolean,
 };
 const result = argv.recursive
   ? grepRecursive(pattern, filePath, options)
@@ -66,14 +71,14 @@ result
     if (argv.count) {
       console.log(grepCount(result));
     } else {
-      printResult(result, argv["line-number"]);
+      printResult(result, argv["line-number"] as boolean);
     }
   })
   .catch((error) => {
     console.error("Error:", error.message);
   });
 
-function printResult(result, showLineNumber) {
+function printResult(result: MatchResult, showLineNumber: boolean) {
   let currentFile = null;
   const fileCount = Object.keys(result).length;

使用 tsc 编译文件:

npx tsc

然后,尝试以下所有命令:

node dist/bin/cmd.js -n result lib/grep.ts
node dist/bin/cmd.js -v "case" lib/grep.ts
node dist/bin/cmd.js -rn "Perform case" .