A collection of well-tested TypeScript utility functions designed for consistency and reusability.
- 🌳 Tree-shakeable - Import only what you need
- 📦 ESM-only - Modern ES modules with full tree-shaking support
- 🔧 TypeScript first - Full type safety
- 🧪 Well tested - Comprehensive test coverage
- 🚀 Zero dependencies - Lightweight and fast
- 🎯 Modern targets - Node.js 18+ and modern browsers
npm install @fiction/utils
# or
pnpm add @fiction/utils
# or
yarn add @fiction/utils// Import only what you need - unused functions will be tree-shaken by your bundler
import {
toCamel,
waitFor,
uuid,
fastHash,
countWords,
randomBetween,
} from "@fiction/utils";
// Or import everything (still tree-shakeable)
import * as utils from "@fiction/utils";import { waitFor, throttle, debounce } from "@fiction/utils";
// Wait for a specific amount of time
await waitFor(1000); // Wait 1 second
// Throttle function calls
const throttledFn = throttle(myFunction, 100);
// Debounce function calls
const debouncedFn = debounce(myFunction, 300);import { getAnonymousId } from "@fiction/utils";
// Get or create anonymous user ID
const { anonymousId, isNew } = getAnonymousId({
anonIdKey: "MyAppAnonId",
firstSessionKey: "MyAppFirstSession",
cookieExpireDays: 365,
});import { toCamel, toPascal, toSnake, toKebab } from "@fiction/utils";
toCamel("hello_world"); // 'helloWorld'
toPascal("hello-world"); // 'HelloWorld'
toSnake("helloWorld"); // 'hello_world'
toKebab("helloWorld"); // 'hello-world'
toSlug("Hello World!"); // 'hello-world'
toLabel("hello_world"); // 'Hello World'
capitalize("hello"); // 'Hello'
// Convert object keys
const obj = { user_name: "john", user_age: 30 };
convertKeyCase(obj, { mode: "camel" }); // { userName: 'john', userAge: 30 }import { uuid, objectId, shortId } from "@fiction/utils";
uuid(); // 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
objectId({ prefix: "usr" }); // 'usr507f1f77bcf86cd799439011'
shortId({ len: 8, withNumbers: true }); // 'a7b2x9k1'import { simpleHandlebarsParser } from "@fiction/utils";
const template = "Hello {{name}}!";
const context = { name: "World" };
simpleHandlebarsParser(template, context); // 'Hello World!'import { fastHash, hashEqual, stringify } from "@fiction/utils";
// Generate consistent hashes for any data structure
fastHash({ name: "John", age: 30 }); // 'a1b2c3d4...'
fastHash([1, 2, 3]); // 'e5f6g7h8...'
// Compare objects by their hash
hashEqual({ a: 1, b: 2 }, { b: 2, a: 1 }); // true (order independent)
hashEqual({ name: "John" }, { name: "Jane" }); // false
// Stringify with circular reference handling
stringify({ user: "data", nested: { deep: "value" } });import { countWords, getObjectWordCount } from "@fiction/utils";
// Count words in text, strips HTML
countWords("Hello <strong>world</strong>!"); // 2
countWords(" Multiple spaces "); // 2
// Count words in object text fields recursively
const obj = {
title: "Blog Post",
content: "This is the main content",
tags: ["javascript", "programming"], // taxonomy fields supported
metadata: { description: "More text here" },
};
getObjectWordCount(obj); // 11
// With custom fields and ignore options
getObjectWordCount(obj, {
addFields: ["customField"],
ignoreKeys: ["metadata"],
});import {
randomBetween,
numberFormatter,
formatNumber,
formatBytes,
durationFormatter,
isNumeric,
} from "@fiction/utils";
// Generate random numbers with decimal precision
randomBetween(1, 10, 2); // 3.47
randomBetween(1, 5, 0); // 3
// Format large numbers with abbreviations
numberFormatter(1500); // '1.5k'
numberFormatter(1000000); // '1.0m'
numberFormatter(1500000000); // '1.5b'
// Comprehensive number formatting
formatNumber(1234, "abbreviated"); // '1.2k'
formatNumber(0.75, "percent"); // '75%'
formatNumber(1234, "dollar"); // '$1,234'
formatNumber(3600, "duration"); // '1h 0m 0s'
// Format file sizes
formatBytes(1024); // '1 KB'
formatBytes(1024 * 1024); // '1 MB'
// Duration formatting
durationFormatter(90, "s"); // '1m 30s'
durationFormatter(3600, "s"); // '1h 0m 0s'
// Check if value is numeric
isNumeric("123"); // true
isNumeric("abc"); // falseimport {
omit,
removeUndefined,
isPlainObject,
parseObject,
setNested,
getNested,
findValueByKey,
deepMerge,
deepMergeAll,
getDotpathArrayIndices,
} from "@fiction/utils";
// Remove specific keys from objects
const obj = { a: 1, b: 2, c: 3 };
omit(obj, "a", "b"); // { c: 3 }
// Clean undefined/null values
removeUndefined({ a: 1, b: undefined, c: null }); // { a: 1, c: null }
removeUndefined({ a: 1, b: undefined, c: null }, { removeNull: true }); // { a: 1 }
// Check for plain objects (not arrays, dates, etc.)
isPlainObject({}); // true
isPlainObject([]); // false
isPlainObject(new Date()); // false
// Transform object values recursively
parseObject({
obj: { a: 1, b: { c: 2 } },
onValue: ({ value }) => (typeof value === "number" ? value * 2 : value),
}); // { a: 2, b: { c: 4 } }
// Set/get nested properties with dot notation
const data = { user: { name: "John" } };
setNested({ data, path: "user.age", value: 30 }); // { user: { name: 'John', age: 30 } }
getNested({ data, path: "user.name" }); // 'John'
// Find values by key recursively
findValueByKey({ a: { b: { target: "found" } } }, "target"); // 'found'
// Deep merge objects
deepMerge([{ a: 1 }, { b: 2 }, { a: 3 }]); // { a: 3, b: 2 }
deepMergeAll([{ items: [1] }, { items: [2] }]); // { items: [1, 2] }
// Extract array indices from dot paths
getDotpathArrayIndices("items.0.title"); // [0]
getDotpathArrayIndices("data.users.2.profile.1"); // [2, 1]import { ObjectProcessor } from "@fiction/utils";
// Create a processor for transforming objects
const processor = new ObjectProcessor();
// Add processors for specific conditions
processor.addProcessor({
condition: ({ value }) => value instanceof Date,
action: (date: Date) => date.toISOString(),
});
processor.addProcessor({
condition: ({ key }) => key === "password",
action: () => "[REDACTED]",
});
// Process objects recursively
const userData = {
name: "John",
password: "secret123",
created: new Date(),
profile: {
lastLogin: new Date(),
settings: { password: "another-secret" },
},
};
const processed = await processor.parseObject(userData);
// Dates converted to ISO strings, passwords redactedimport { Shortcodes } from "@fiction/utils";
// Create shortcode processor
const processor = new Shortcodes();
// Add custom shortcodes
processor.addShortcode({
shortcode: "user",
handler: ({ attributes }) => `Hello ${attributes?.name || "Anonymous"}!`,
});
processor.addShortcode({
shortcode: "upper",
handler: ({ content }) => content?.toUpperCase() || "",
});
// Process strings with shortcodes
await processor.parseString('[@user name="John"] [@upper]welcome[/@upper]');
// Result: "Hello John! WELCOME"
// Process objects with shortcodes in strings
await processor.parseObject({
title: "[@upper]my title[/@upper]",
greeting: '[@user name="Jane"]',
metadata: { author: '[@user name="Admin"]' },
});
// All shortcodes in string values are processed
// Built-in shortcodes: [@date], [@time], [@timestamp], [@cwd]
await processor.parseString("Report generated on [@date] at [@time]");import { uiReset } from "@fiction/utils";
// Register reset callbacks for modals, dropdowns, etc.
const cleanup = uiReset.onReset(() => {
closeModal();
hideDropdown();
clearTooltips();
});
// Cleanup when component unmounts (Vue example)
onUnmounted(cleanup);
// Manual reset trigger
uiReset.reset();
// Framework examples:
// Vue 3
import { onUnmounted } from 'vue';
const cleanup = uiReset.onReset(() => setShowModal(false));
onUnmounted(cleanup);
// React
import { useEffect } from 'react';
useEffect(() => {
const cleanup = uiReset.onReset(() => setShowModal(false));
return cleanup; // Cleanup on unmount
}, []);
// Astro/Vanilla JS
const cleanup = uiReset.onReset(() => {
document.getElementById('modal').style.display = 'none';
});
// Call cleanup() when neededFeatures:
- ✅ Framework agnostic - Works with Vue, React, Astro, vanilla JS
- ✅ SSR safe - No-op functions when
windowis undefined - ✅ Standard browser APIs - Uses
historyinterception +popstatefor route changes - ✅ Auto-triggers - Escape key, window clicks, route navigation
- ✅ Error handling - Catches callback errors without breaking others
# Install dependencies
pnpm install
# Run tests
pnpm test
# Run tests with coverage
pnpm test:coverage
# Build the package
pnpm build
# Lint code
pnpm lint
# Type check
pnpm typecheckThis package uses automated publishing via GitHub Actions with two workflows:
- Trigger: Every push to
mainbranch - Process: CI runs → Tests pass → Version bumped (patch) → Published to NPM
- Use case: Bug fixes, documentation updates, minor improvements
- Trigger: Manual workflow dispatch in GitHub Actions
- Process: Choose version bump type (patch/minor/major) → Tests pass → Version bumped → Tagged → Published to NPM
- Use case: New features (minor), breaking changes (major)
- Go to Actions tab in GitHub
- Select "Manual Release" workflow
- Click "Run workflow"
- Choose version bump type (patch/minor/major)
- GitHub Actions will handle the rest
For the automated workflows to work, you need to set up these repository secrets:
NPM_TOKEN- Your NPM authentication token for publishingDISCORD_WEBHOOK_URL(optional) - Discord webhook URL for release notifications
The workflows will automatically post to Discord when:
- ✅ Releases are successful or fail
- ✅ CI builds pass or fail on the main branch
To set up Discord notifications:
- Create a Discord webhook in your server
- Add the webhook URL to your GitHub repository secrets as
DISCORD_WEBHOOK_URL
MIT