GitHub - GoodbyeNJN/utils
A modern TypeScript/JavaScript utility library providing a comprehensive collection of type-safe utility functions, functional error handling with the Result and Option patterns, filesystem operations, and shell command execution.
Features
- ๐ Modern: Built with TypeScript, targeting ES modules and modern JavaScript
- ๐ Type-safe: Full TypeScript support with comprehensive type definitions and type inference
- ๐ฆ Modular: Import only what you need with tree-shakable exports and multiple entry points
- ๐ก๏ธ Result & Option Pattern: Functional error handling and optional values without exceptions, based on Rust-style Result and Option types
- ๐ VFile & FS: Type-safe file system operations and a powerful virtual file object
- ๐ Exec: Powerful and flexible command execution with safe and unsafe variants
- ๐งฐ Common Utilities: String manipulation, math operations, promise utilities, and JSON handling
- ๐ FP Utilities: Functional programming utilities built on top of Remeda and Rotery, including async iteration helpers
Installation
npm install @goodbyenjn/utils # or pnpm add @goodbyenjn/utils # or yarn add @goodbyenjn/utils
Usage
Quick Start
// Import what you need from the main module import { sleep, template } from "@goodbyenjn/utils"; import { exec } from "@goodbyenjn/utils/exec"; import { exec as safeExec } from "@goodbyenjn/utils/exec/safe"; import { BaseVFile } from "@goodbyenjn/utils/fs"; import { readFile as safeReadFile } from "@goodbyenjn/utils/fs/safe"; import { Ok, Result } from "@goodbyenjn/utils/result"; import { parse } from "@goodbyenjn/utils/json"; import { parse as safeParse } from "@goodbyenjn/utils/json/safe"; import { Some, None } from "@goodbyenjn/utils/option"; import type { Nullable, SetOptional } from "@goodbyenjn/utils/types"; // Enable global type augmentations (better Object.keys, Map.has, Array.filter, etc.) import "@goodbyenjn/utils/global-types";
Common Utilities
String Operations
import { template, unindent, addPrefix, removeSuffix, joinWith, splitBy } from "@goodbyenjn/utils"; // String templating const greeting = template("Hello, {name}! You are {age} years old.", { name: "Alice", age: 30, }); console.log(greeting); // "Hello, Alice! You are 30 years old." // Remove common indentation from template strings (default: trim start and end) const code = unindent` function example() { return 'formatted'; } `; console.log(code); /* function example() { return 'formatted'; } */ // Custom trim behavior with factory function const codeNoTrim = unindent(false, false)` function example() { return 'formatted'; } `; console.log(codeNoTrim); /* function example() { return 'formatted'; } */ // Only trim start, keep end const onlyTrimStart = unindent(true, false)(" hello\n world\n"); console.log(onlyTrimStart); // "hello\nworld\n" // Add indentation to strings const indented = indent(2)` if (a) { b() } `; console.log(indented); /* if (a) { b() } */ // With custom string const arrowIndented = indent(">>")("line1\nline2"); console.log(arrowIndented); // ">>line1\n>>line2" // Only trim start, keep end const onlyTrimStart = indent(2, true, false)("hello\nworld\n"); console.log(onlyTrimStart); // " hello\n world\n" // Prefix and suffix operations const withPrefix = addPrefix("@", "myfile"); // "@myfile" const cleaned = removeSuffix(".js", "script.js"); // "script" // String joining and splitting const path = joinWith("/", "home", "user", "docs"); // "/home/user/docs" const parts = splitBy("-", "hello-world-js"); // ["hello", "world", "js"] // Split string by line breaks (handles both \n and \r\n) const lines = splitByLineBreak("line1\nline2\r\nline3"); console.log(lines); // ["line1", "line2", "line3"] // Parse boolean value with custom default const isEnabled = parseValueToBoolean("yes", false); // true const debugMode = parseValueToBoolean("invalid", "auto"); // "auto"
Promise Utilities
import { sleep, createLock, createSingleton, createPromiseWithResolvers } from "@goodbyenjn/utils"; // Sleep/delay execution await sleep(1000); // Wait 1 second // Create a reusable mutex lock const lock = createLock(); await lock.acquire(); try { // Critical section console.log("Executing exclusively"); } finally { lock.release(); } // Singleton pattern factory const getDatabase = createSingleton(() => { console.log("Initializing database..."); return new Database(); }); const db1 = await getDatabase(); // Initializes once const db2 = await getDatabase(); // Returns same instance // Promise with external resolvers const { promise, resolve, reject } = createPromiseWithResolvers<string>(); setTimeout(() => resolve("done!"), 1000); const result = await promise;
Command Execution
import { exec } from "@goodbyenjn/utils/exec"; import { exec as safeExec } from "@goodbyenjn/utils/exec/safe"; // 1. Unsafe Execution (throws on failure) const output = await exec`npm install`; console.log(output.stdout); // String command with args const lsOutput = await exec("ls", ["-la"]); console.log(lsOutput.stdout); // 2. Safe Execution (returns Result) const safeOutput = await safeExec`npm install`; if (safeOutput.isOk()) { console.log("Success:", safeOutput.unwrap().stdout); } else { // Result contains error information (e.g., NonZeroExitError) console.error("Failed:", safeOutput.unwrapErr().message); } // 3. Pipe Output const piped = await exec`echo "hello"`.pipe`cat`; console.log(piped.stdout); // 4. Iterate Output for await (const line of exec`cat large-file.txt`) { console.log(line); } // 5. Configuration Factory const withCwd = exec({ cwd: "/path/to/project" }); const result3 = await withCwd`npm install`;
Math Utilities
import { linear, scale } from "@goodbyenjn/utils"; // Linear interpolation between values const mid = linear(0.5, [0, 100]); // 50 // Scale a value from one range to another const scaledValue = scale(75, [0, 100], [0, 1]); // 0.75 const percentage = scale(200, [0, 255], [0, 100]); // 78.43...
Error Handling
import { normalizeError, getErrorMessage } from "@goodbyenjn/utils"; // Normalize any value to an Error object const error = normalizeError("Something went wrong"); const typeError = normalizeError({ code: 500 }); // Safely extract error message const message1 = getErrorMessage(new Error("Oops")); const message2 = getErrorMessage("Plain string error"); const message3 = getErrorMessage(null); // "Unknown error"
Throttling and Debouncing
import { debounce, throttle } from "@goodbyenjn/utils/fp"; // Debounce - wait for inactivity before executing const debouncedSearch = debounce((query: string) => { console.log("Searching for:", query); }, 300); // Call multiple times, executes only after 300ms of inactivity input.addEventListener("input", e => { debouncedSearch((e.target as HTMLInputElement).value); }); // Throttle - execute at most once per interval const throttledScroll = throttle(() => { console.log("Scroll position:", window.scrollY); }, 100); window.addEventListener("scroll", throttledScroll);
JSON Handling
import { parse, stringify } from "@goodbyenjn/utils/json"; // Or use the safe-only entry point: import { parse as safeParse, stringify as safeStringify } from "@goodbyenjn/utils/json/safe"; // Standard JSON parsing (returns value or nil) const data = parse('{"a": 1}'); // Some({ a: 1 }) const invalid = parse("bad"); // None // Safe JSON parsing (returns Result) const result = safeParse('{"a": 1}'); if (result.isOk()) { console.log(result.unwrap().a); } // Safe stringify const json = safeStringify({ a: 1 }); // Result<string, Error>
File System Operations
import { BaseVFile, exists as safeExists, mkdir as safeMkdir, readFile as safeReadFile, readFileByLine as safeReadFileByLine, readJson as safeReadJson, rm as safeRm, writeFile as safeWriteFile, writeJson as safeWriteJson, } from "@goodbyenjn/utils/fs/safe"; // BaseVFile - Unified file handling const vfile = new BaseVFile("example.json"); // Fluid path manipulation vfile.filename("data").extname("ts"); console.log(vfile.basename()); // "data.ts" // Cross-platform path handling const relative = vfile.pathname.relative(); // "data.ts" (relative to cwd) const absolute = vfile.pathname(); // "/full/path/to/data.ts" // Built-in operations (available in extended VFile implementations) // await vfile.read(); // Get content // await vfile.write(); // Write content const textResult = await safeReadFile("example.txt"); if (textResult.isOk()) { console.log("File content:", textResult.unwrap()); } else { console.error("Failed to read file:", textResult.unwrapErr().message); } // Read and parse JSON safely const jsonResult = await safeReadJson("package.json"); if (jsonResult.isOk()) { const pkg = jsonResult.unwrap(); console.log("Package name:", pkg.name); } // Write JSON file const writeResult = await safeWriteJson("data.json", { users: [] }, 2); if (writeResult.isErr()) { console.error("Write failed:", writeResult.unwrapErr()); } // Check if file exists const exists = await safeExists("path/to/file.txt"); if (exists) { console.log("File exists!"); } // Create directories (recursive) const mkResult = await safeMkdir("src/components/ui", { recursive: true }); // Delete files or directories const rmResult = await safeRm("build", { recursive: true, force: true }); // Read file line by line const lineResult = await safeReadFileByLine("large-file.log"); if (lineResult.isOk()) { for await (const line of lineResult.unwrap()) { console.log(line); } }
Glob Patterns
import { glob, globSync, convertPathToPattern } from "@goodbyenjn/utils/glob"; // Async glob pattern matching const files = await glob("src/**/*.{ts,tsx}", { cwd: "." }); console.log("Found files:", files); // Synchronous version const syncFiles = globSync("**/*.test.ts", { cwd: "tests" }); // Convert file path to glob pattern const pattern = convertPathToPattern("/home/user/project");
Result Pattern - Functional Error Handling
import { Err, Ok, Result } from "@goodbyenjn/utils/result"; // Create results explicitly const success = Ok(42); const failure = Err("Something went wrong"); // Handle results with chainable methods const doubled = success .map(value => value * 2) // Supports async: .map(async v => v * 2) returns Promise<Result> .mapErr(err => `Error: ${err}`) .unwrapOr(0); // 84 // Transform error type const result: Result<string, Error> = Ok("value"); const transformed = result.mapErr(() => new Error("Custom error")); // Convert throwing functions or promises to Result async function fetchUser(id: string) { // Result.try catches thrown errors const user = await Result.try(() => JSON.parse(userJson)); // Or handle promise rejections const user = await Result.try(fetch(`/api/users/${id}`)); return user.map(u => u.name).mapErr(err => new Error(`Failed to parse user: ${err.message}`)); } // Wrap a function to always return a Result const safeParse = Result.wrap(JSON.parse, Error); const data = safeParse('{"valid": true}'); // Result<any, Error> // Combine multiple Results const results = [Ok(1), Ok(2), Err("oops"), Ok(4)]; const combined = Result.all(results); // Err("oops") // Generator-based "do" notation for flattening Results const finalResult = Result.gen(function* () { const a = yield* Ok(10); const b = yield* Ok(20); return a + b; }); // Ok(30) // Supports async generators const asyncFinal = await Result.gen(async function* () { const user = yield* await fetchUser("1"); return user.name; });
Type Utilities
import type { Nullable, Optional, YieldType, OmitByKey, SetNullable, TemplateFn, // New function types with `this` binding FnWithThis, AsyncFnWithThis, SyncFnWithThis, // Re-exported from type-fest SetOptional, SetRequired, PartialDeep, Simplify, LiteralUnion, PackageJson, } from "@goodbyenjn/utils/types"; // ... (other types) // Template string function type const myTag: TemplateFn<string> = (strings, ...values) => { return strings[0] + values[0]; }; // Nullable type for values that can be null or undefined type User = { id: string; name: string; email: Nullable<string>; // string | null | undefined }; // Optional type (undefined but not null) type Profile = { bio: Optional<string>; // string | undefined }; // Extract yield type from generators function\* numberGenerator() { yield 1; yield 2; yield 3; } type NumberType = YieldType<typeof numberGenerator>; // number // Omit properties by their value type type Config = { name: string; debug: boolean; verbose: boolean; timeout: number; }; type WithoutBooleans = OmitByKey<Config, boolean>; // { name: string; timeout: number } // Set specific properties to nullable type APIResponse = { id: number; name: string; email: string; }; type PartialResponse = SetNullable<APIResponse, "email" | "name">; // email and name become nullable
FP Utilities
The library provides 100+ functional utilities via @goodbyenjn/utils/fp, built on top of Remeda and Rotery:
import { // Custom type-checking helpers hasOwnProperty, isFunction, isPromiseLike, isOption, // check if a value is an Option isResult, // check if a value is a Result // Throttle / debounce (moved from main module) debounce, throttle, // Array operations (from Remeda) chunk, filter, find, flatMap, flatten, map, partition, reverse, take, drop, unique, // Object operations (from Remeda) pick, omit, merge, keys, values, entries, // Functional composition (from Remeda) pipe, compose, // Aggregations (from Remeda) groupBy, countBy, sumBy, // Async iteration (from Rotery, aliased with P suffix) filterP, // async filter mapP, // async map flatMapP, // async flatMap forEachP, // async forEach every, // sync every everyP, // async every some, // sync some someP, // async some toArray, // collect iterator to array toArrayP, // async collect flatten as flattenSync, flattenP, // async flatten reduceP, // async reduce concurrency, // limit concurrency buffer, // buffer items } from "@goodbyenjn/utils/fp"; // Type-safe property checking const obj = { name: "John", age: 30, active: true }; if (hasOwnProperty(obj, "name")) { console.log(obj.name); // TypeScript type narrowing } // Function type checking const maybeCallback: unknown = (x: number) => x * 2; if (isFunction(maybeCallback)) { maybeCallback(5); } // Promise detection async function handleValue(value: any) { if (isPromiseLike(value)) { const result = await value; console.log("Async result:", result); } } // Functional data transformations const users = [ { id: 1, name: "Alice", role: "admin", active: true }, { id: 2, name: "Bob", role: "user", active: false }, { id: 3, name: "Charlie", role: "user", active: true }, ]; // Chain operations with pipe const adminNames = pipe( users, filter(u => u.role === "admin"), map(u => u.name), ); // ["Alice"] // Group users by role const byRole = groupBy(users, u => u.role); // { admin: [...], user: [...] } // Sum ages of active users const totalAge = sumBy( filter(users, u => u.active), u => u.age ?? 0, ); // Chunk array into groups const chunked = chunk(users, 2); // [[user1, user2], [user3]] // Async iteration with Rotery helpers const results = await pipe( [1, 2, 3], toIterator, mapP(async n => fetchUser(n)), filterP(async u => u.active), toArrayP, );
Requirements
- Node.js: >= 20.0.0
- TypeScript: >= 6.0 (for development/type checking)
Modern browsers are supported through ES module imports.
Versioning
Note: This project does not follow Semantic Versioning (semver). Instead, it uses a calendar-based versioning scheme:
Version Format: v<YY>.<M>.<PATCH>
<YY>- Release year (e.g., 26 for 2026)<M>- Release month (1-12)<PATCH>- Patch/revision number within the same month (starting from 0)
Example versions:
v26.1.0- First release in January 2026v26.1.1- Second release in January 2026v26.2.0- First release in February 2026
This scheme provides clarity on when features were released while allowing multiple updates within the same month.
Development
# Install dependencies pnpm install # Development mode with watch pnpm run dev # Build the library pnpm run build # Clean build artifacts pnpm run clean # Run tests (if configured) pnpm run test
Performance Considerations
- Tree-shaking: All modules are properly configured for tree-shaking. Import only what you need.
- Result Pattern: The Result type has minimal overhead compared to exceptions and enables better error handling.
- Functional Composition: Use Remeda utilities with pipe for efficient data transformations.
- Shell Execution: The
$function safely escapes arguments and is suitable for production use.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request at GitHub Repository.
License
MIT ยฉ GoodbyeNJN
Links
Maintained with โค๏ธ by GoodbyeNJN