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.
- 🚀 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
npm install @goodbyenjn/utils
# or
pnpm add @goodbyenjn/utils
# or
yarn add @goodbyenjn/utils// Import what you need from the main moduleimport{sleep,template}from"@goodbyenjn/utils";import{exec}from"@goodbyenjn/utils/exec";import{execassafeExec}from"@goodbyenjn/utils/exec/safe";import{BaseVFile}from"@goodbyenjn/utils/fs";import{readFileassafeReadFile}from"@goodbyenjn/utils/fs/safe";import{Ok,Result}from"@goodbyenjn/utils/result";import{parse}from"@goodbyenjn/utils/json";import{parseassafeParse}from"@goodbyenjn/utils/json/safe";import{Some,None}from"@goodbyenjn/utils/option";importtype{Nullable,SetOptional}from"@goodbyenjn/utils/types";// Enable global type augmentations (better Object.keys, Map.has, Array.filter, etc.)import"@goodbyenjn/utils/global-types";import{template,unindent,addPrefix,removeSuffix,joinWith,splitBy}from"@goodbyenjn/utils";// String templatingconstgreeting=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)constcode=unindent` function example() { return 'formatted'; }`;console.log(code);/*function example() { return 'formatted';}*/// Custom trim behavior with factory functionconstcodeNoTrim=unindent(false,false)` function example() { return 'formatted'; }`;console.log(codeNoTrim);/*function example() { return 'formatted';}*/// Only trim start, keep endconstonlyTrimStart=unindent(true,false)(" hello\n world\n");console.log(onlyTrimStart);// "hello\nworld\n"// Add indentation to stringsconstindented=indent(2)` if (a) { b() }`;console.log(indented);/* if (a) { b() }*/// With custom stringconstarrowIndented=indent(">>")("line1\nline2");console.log(arrowIndented);// ">>line1\n>>line2"// Only trim start, keep endconstonlyTrimStart=indent(2,true,false)("hello\nworld\n");console.log(onlyTrimStart);// " hello\n world\n"// Prefix and suffix operationsconstwithPrefix=addPrefix("@","myfile");// "@myfile"constcleaned=removeSuffix(".js","script.js");// "script"// String joining and splittingconstpath=joinWith("/","home","user","docs");// "/home/user/docs"constparts=splitBy("-","hello-world-js");// ["hello", "world", "js"]// Split string by line breaks (handles both \n and \r\n)constlines=splitByLineBreak("line1\nline2\r\nline3");console.log(lines);// ["line1", "line2", "line3"]// Parse boolean value with custom defaultconstisEnabled=parseValueToBoolean("yes",false);// trueconstdebugMode=parseValueToBoolean("invalid","auto");// "auto"import{sleep,createLock,createSingleton,createPromiseWithResolvers}from"@goodbyenjn/utils";// Sleep/delay executionawaitsleep(1000);// Wait 1 second// Create a reusable mutex lockconstlock=createLock();awaitlock.acquire();try{// Critical sectionconsole.log("Executing exclusively");}finally{lock.release();}// Singleton pattern factoryconstgetDatabase=createSingleton(()=>{console.log("Initializing database...");returnnewDatabase();});constdb1=awaitgetDatabase();// Initializes onceconstdb2=awaitgetDatabase();// Returns same instance// Promise with external resolversconst{ promise, resolve, reject }=createPromiseWithResolvers<string>();setTimeout(()=>resolve("done!"),1000);constresult=awaitpromise;import{exec}from"@goodbyenjn/utils/exec";import{execassafeExec}from"@goodbyenjn/utils/exec/safe";// 1. Unsafe Execution (throws on failure)constoutput=awaitexec`npm install`;console.log(output.stdout);// String command with argsconstlsOutput=awaitexec("ls",["-la"]);console.log(lsOutput.stdout);// 2. Safe Execution (returns Result)constsafeOutput=awaitsafeExec`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 Outputconstpiped=awaitexec`echo "hello"`.pipe`cat`;console.log(piped.stdout);// 4. Iterate Outputforawait(constlineofexec`cat large-file.txt`){console.log(line);}// 5. Configuration FactoryconstwithCwd=exec({cwd: "/path/to/project"});constresult3=awaitwithCwd`npm install`;import{linear,scale}from"@goodbyenjn/utils";// Linear interpolation between valuesconstmid=linear(0.5,[0,100]);// 50// Scale a value from one range to anotherconstscaledValue=scale(75,[0,100],[0,1]);// 0.75constpercentage=scale(200,[0,255],[0,100]);// 78.43...import{normalizeError,getErrorMessage}from"@goodbyenjn/utils";// Normalize any value to an Error objectconsterror=normalizeError("Something went wrong");consttypeError=normalizeError({code: 500});// Safely extract error messageconstmessage1=getErrorMessage(newError("Oops"));constmessage2=getErrorMessage("Plain string error");constmessage3=getErrorMessage(null);// "Unknown error"import{debounce,throttle}from"@goodbyenjn/utils/fp";// Debounce - wait for inactivity before executingconstdebouncedSearch=debounce((query: string)=>{console.log("Searching for:",query);},300);// Call multiple times, executes only after 300ms of inactivityinput.addEventListener("input",e=>{debouncedSearch((e.targetasHTMLInputElement).value);});// Throttle - execute at most once per intervalconstthrottledScroll=throttle(()=>{console.log("Scroll position:",window.scrollY);},100);window.addEventListener("scroll",throttledScroll);import{parse,stringify}from"@goodbyenjn/utils/json";// Or use the safe-only entry point:import{parseassafeParse,stringifyassafeStringify}from"@goodbyenjn/utils/json/safe";// Standard JSON parsing (returns value or nil)constdata=parse('{"a": 1}');// Some({ a: 1 })constinvalid=parse("bad");// None// Safe JSON parsing (returns Result)constresult=safeParse('{"a": 1}');if(result.isOk()){console.log(result.unwrap().a);}// Safe stringifyconstjson=safeStringify({a: 1});// Result<string, Error>import{BaseVFile,existsassafeExists,mkdirassafeMkdir,mkdtempassafeMkdtemp,readFileassafeReadFile,readFileByLineassafeReadFileByLine,readJsonassafeReadJson,rmassafeRm,writeFileassafeWriteFile,writeJsonassafeWriteJson,}from"@goodbyenjn/utils/fs/safe";// BaseVFile - Unified file handlingconstvfile=newBaseVFile("example.json");// Fluid path manipulationvfile.filename("data").extname("ts");console.log(vfile.basename());// "data.ts"// Cross-platform path handlingconstrelative=vfile.pathname.relative();// "data.ts" (relative to cwd)constabsolute=vfile.pathname();// "/full/path/to/data.ts"// Built-in operations (available in extended VFile implementations)// await vfile.read(); // Get content// await vfile.write(); // Write contentconsttextResult=awaitsafeReadFile("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 safelyconstjsonResult=awaitsafeReadJson("package.json");if(jsonResult.isOk()){constpkg=jsonResult.unwrap();console.log("Package name:",pkg.name);}// Write JSON fileconstwriteResult=awaitsafeWriteJson("data.json",{users: []},2);if(writeResult.isErr()){console.error("Write failed:",writeResult.unwrapErr());}// Check if file existsconstexists=awaitsafeExists("path/to/file.txt");if(exists){console.log("File exists!");}// Create directories (recursive)constmkResult=awaitsafeMkdir("src/components/ui",{recursive: true});// Create a temporary directoryconsttempResult=awaitsafeMkdtemp("my-temp-dir-");// Or with disposable optionconsttempDisposableResult=awaitsafeMkdtemp("my-temp-dir-",{disposable: true});// Delete files or directoriesconstrmResult=awaitsafeRm("build",{recursive: true,force: true});// Read file line by lineconstlineResult=awaitsafeReadFileByLine("large-file.log");if(lineResult.isOk()){forawait(constlineoflineResult.unwrap()){console.log(line);}}import{glob,globSync,convertPathToPattern}from"@goodbyenjn/utils/glob";// Async glob pattern matchingconstfiles=awaitglob("src/**/*.{ts,tsx}",{cwd: "."});console.log("Found files:",files);// Synchronous versionconstsyncFiles=globSync("**/*.test.ts",{cwd: "tests"});// Convert file path to glob patternconstpattern=convertPathToPattern("/home/user/project");import{Err,Ok,Result}from"@goodbyenjn/utils/result";// Create results explicitlyconstsuccess=Ok(42);constfailure=Err("Something went wrong");// Handle results with chainable methodsconstdoubled=success.map(value=>value*2)// Supports async: .map(async v => v * 2) returns Promise<Result>.mapErr(err=>`Error: ${err}`).unwrapOr(0);// 84// Transform error typeconstresult: Result<string,Error>=Ok("value");consttransformed=result.mapErr(()=>newError("Custom error"));// Convert throwing functions or promises to ResultasyncfunctionfetchUser(id: string){// Result.try catches thrown errorsconstuser=awaitResult.try(()=>JSON.parse(userJson));// Or handle promise rejectionsconstuser=awaitResult.try(fetch(`/api/users/${id}`));returnuser.map(u=>u.name).mapErr(err=>newError(`Failed to parse user: ${err.message}`));}// Wrap a function to always return a ResultconstsafeParse=Result.wrap(JSON.parse,Error);constdata=safeParse('{"valid": true}');// Result<any, Error>// Combine multiple Resultsconstresults=[Ok(1),Ok(2),Err("oops"),Ok(4)];constcombined=Result.all(results);// Err("oops")// Generator-based "do" notation for flattening ResultsconstfinalResult=Result.gen(function*(){consta=yield*Ok(10);constb=yield*Ok(20);returna+b;});// Ok(30)// Supports async generatorsconstasyncFinal=awaitResult.gen(asyncfunction*(){constuser=yield*awaitfetchUser("1");returnuser.name;});importtype{Nullable,Optional,YieldType,OmitByKey,SetNullable,TemplateFn,// New function types with `this` bindingFnWithThis,AsyncFnWithThis,SyncFnWithThis,// Re-exported from type-festSetOptional,SetRequired,PartialDeep,Simplify,LiteralUnion,PackageJson,}from"@goodbyenjn/utils/types";// ... (other types)// Template string function typeconstmyTag: TemplateFn<string>=(strings, ...values)=>{returnstrings[0]+values[0];};// Nullable type for values that can be null or undefinedtypeUser={id: string;name: string;email: Nullable<string>;// string | null | undefined};// Optional type (undefined but not null)typeProfile={bio: Optional<string>;// string | undefined};// Extract yield type from generatorsfunction\* numberGenerator(){yield1;yield2;yield3;}typeNumberType=YieldType<typeofnumberGenerator>;// number// Omit properties by their value typetypeConfig={name: string;debug: boolean;verbose: boolean;timeout: number;};typeWithoutBooleans=OmitByKey<Config,boolean>;// { name: string; timeout: number }// Set specific properties to nullabletypeAPIResponse={id: number;name: string;email: string;};typePartialResponse=SetNullable<APIResponse,"email"|"name">;// email and name become nullableThe library provides 100+ functional utilities via @goodbyenjn/utils/fp, built on top of Remeda and Rotery:
import{// Custom type-checking helpershasOwnProperty,isFunction,isPromiseLike,isOption,// check if a value is an OptionisResult,// 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 filtermapP,// async mapflatMapP,// async flatMapforEachP,// async forEachevery,// sync everyeveryP,// async everysome,// sync somesomeP,// async sometoArray,// collect iterator to arraytoArrayP,// async collectflattenasflattenSync,flattenP,// async flattenreduceP,// async reduceconcurrency,// limit concurrencybuffer,// buffer items}from"@goodbyenjn/utils/fp";// Type-safe property checkingconstobj={name: "John",age: 30,active: true};if(hasOwnProperty(obj,"name")){console.log(obj.name);// TypeScript type narrowing}// Function type checkingconstmaybeCallback: unknown=(x: number)=>x*2;if(isFunction(maybeCallback)){maybeCallback(5);}// Promise detectionasyncfunctionhandleValue(value: any){if(isPromiseLike(value)){constresult=awaitvalue;console.log("Async result:",result);}}// Functional data transformationsconstusers=[{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 pipeconstadminNames=pipe(users,filter(u=>u.role==="admin"),map(u=>u.name),);// ["Alice"]// Group users by roleconstbyRole=groupBy(users,u=>u.role);// { admin: [...], user: [...] }// Sum ages of active usersconsttotalAge=sumBy(filter(users,u=>u.active),u=>u.age??0,);// Chunk array into groupsconstchunked=chunk(users,2);// [[user1, user2], [user3]]// Async iteration with Rotery helpersconstresults=awaitpipe([1,2,3],toIterator,mapP(asyncn=>fetchUser(n)),filterP(asyncu=>u.active),toArrayP,);- Node.js: >= 20.0.0
- TypeScript: >= 6.0 (for development/type checking)
Modern browsers are supported through ES module imports.
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.
# 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- 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.
Contributions are welcome! Please feel free to submit a Pull Request at GitHub Repository.
MIT © GoodbyeNJN
Maintained with ❤️ by GoodbyeNJN