» Make grep CLI App in Node.js » 2. Development » 2.4 Add Types

Add Type Annotations

You can add type annotations and leverage TypeScript to introduce static typing. TypeScript is a superset of JavaScript that includes static typing, and it can be used with Node.js projects to catch type-related errors during development.

If you haven't already, Install TypeScript as a development dependency in your project:

npm install --save-dev typescript

Run the following command to generate a tsconfig.json file, which is the TypeScript configuration file:

# tsc stands for "TypeScript compiler"
npx tsc --init

npx stands for "Node Package eXecute." It is a command-line tool that comes with npm and is used for executing Node.js packages. The primary purpose of npx is to make it easy to run binaries from npm packages without the need to install them globally.

tsconfig.json:

{
  "compilerOptions": {
    "target": "es2017",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "module": "ESNext",                                  /* Specify what module code is generated. */
    "rootDir": "./",                                     /* Specify the root folder within your source files. */
    "outDir": "./dist",                                  /* Specify an output folder for all emitted files. */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
    /* Type Checking */
    "strict": true,                                      /* Enable all strict type-checking options. */
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  },
  "include": [
    "bin/*.ts",
    "lib/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

Update package.json:

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

It is used to indicate that your JavaScript files are ES6 modules. This means that the files are using the ECMAScript module syntax (import and export statements) rather than the CommonJS syntax (require and module.exports).

TypeScript uses type definition files (.d.ts) to provide type information for libraries and modules. Install the Node.js type definitions:

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

Compile your TypeScript code to JavaScript using the TypeScript compiler (tsc). Run the following command:

npx tsc

This will generate JavaScript files in the specified output directory.

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 will report a lot of type errors about your code, try to fix all of them.

Change the .js extension to .ts, and modify 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 is an alias of type { [key: string]: MatchItem[]; }, which represents something like this:

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

Change bin/grepjs to bin/cmd.ts, and modify it:

@@ -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;

Use tsc to compile the files:

npx tsc

Then, try all the commands as below:

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" .
PrevNext