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 withnpm
and is used for executing Node.js packages. The primary purpose ofnpx
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" .