duckkit
v0.1.7
Published
TypeScript-first utility library. Zero dependencies, tree-shakeable, fully typed. Typed groupBy, safe/Result type, pipe, pipeline, compose, curry, typed event emitter, delaySkippable, and more.
Downloads
940
Maintainers
Keywords
Readme
duckkit 🦆
TypeScript-first utility library. Zero dependencies, tree-shakeable, fully typed.
Covers array, object, string, number, date, async, delay, emitter, encode, validate, and function composition utilities. Each function is properly typed — no any, no Record<string, unknown> workarounds. Import everything or per category, only what you use ends up in the bundle.
Overview
A comprehensive TypeScript utility library that provides:
- Zero Dependencies — lightweight and self-contained, nothing pulled in
- Tree-Shakeable — import per category, only what you use ends up in the bundle
- Fully Typed — no
any, noRecord<string, unknown>workarounds, proper generics throughout - ESM + CJS — works in Node.js, browsers, and bundlers
- Typed
groupBy— returnsRecord<K, T[]>, notDictionary<any> deepClonethat preserves Dates — unlike theJSON.parse(JSON.stringify(...))tricksafe/ Result type — try/catch as a typed value, no untyped throwsdelaySkippable— cancellable async wait, unique to duckkit- Typed event emitter — payload types enforced at compile time
- Function composition —
pipeline,compose,curry,tap,whenwith full type inference - Encoding utilities — Base64, Base64URL, Hex, URI, JSON — all with safe error handling
- Validation utilities — format validators, type guards, value checks — all return
boolean
Install
npm install duckkitAvailable Utilities
| Category | Functions |
|----------|-----------|
| Array | groupBy() flatGroupBy() sortBy() topBy() minBy() maxBy() partition() chunk() unique() zip() flatten() range() compact() sample() shuffle() without() union() intersection() difference() countBy() keyBy() sum() sumBy() |
| Object | pick() omit() deepClone() deepMerge() isEqual() mapKeys() mapValues() invertObject() flattenObject() unflattenObject() filterKeys() filterValues() keys() values() entries() fromEntries() deepFreeze() objectDiff() mergeIf() hasOwn() |
| Async | safe() safeAsync() pipe() memo() memoAsync() debounce() throttle() retry() timeout() once() defer() parallel() sequential() |
| Date | timeAgo() formatDate() daysBetween() addDays() subDays() addMonths() addYears() isBefore() isAfter() isSameDay() startOfDay() endOfDay() startOfWeek() startOfMonth() isToday() isYesterday() isWeekend() isThisWeek() isThisYear() |
| Number | clamp() lerp() roundTo() truncateTo() randomInt() inRange() average() normalize() toOrdinal() toRoman() formatNumber() formatBytes() formatDuration() |
| String | capitalize() truncate() excerpt() slugify() camelCase() snakeCase() kebabCase() pascalCase() titleCase() isEmpty() randomId() countOccurrences() escapeHtml() unescapeHtml() template() words() mask() stripHtml() |
| Delay | delay() delaySkippable() delayWithAbort() repeat() |
| Emitter | createEmitter() — with on() off() once() emit() clear() |
| Encode | encodeBase64() decodeBase64() encodeBase64Url() decodeBase64Url() encodeHex() decodeHex() encodeUri() decodeUri() encodeJson() decodeJson() |
| Validate | isEmail() isUrl() isUUID() isIP() isIPv4() isIPv6() isCreditCard() isPhone() isString() isNumber() isBoolean() isArray() isObject() isDate() isFunction() isNull() isUndefined() isNullOrUndefined() isPromise() isEmpty() isEmptyObject() isEnum() |
| Fn | pipeline() compose() pipelineAsync() composeAsync() curry() tap() when() |
| Result types | safe() safeAsync() — returns Ok<T> | Err, import Result, Ok, Err types |
Key Features
Array Utilities
- Typed
groupBy— returnsRecord<K, T[]>, notany partition,topBy,minBy,maxBy,chunk,compact,uniqueshuffle,flatten,range,zip,without,union,intersection,differencecountBy,keyBy,sum,sumBy,sample
Object Utilities
deepClone— preservesDateobjects, unlikeJSON.parse(JSON.stringify(...))deepMerge— nested merge with Date support, non-mutatingpick,omit— removed keys disappear from the TypeScript type entirelydeepFreeze— recursively freezes and returnsDeepReadonly<T>objectDiff— returns keys that changed between two objectsmergeIf— merges only defined values, skipsnullandundefinedhasOwn— safe typed wrapper forObject.hasOwnflattenObject,unflattenObject,invertObjectmapKeys,mapValues,filterKeys,filterValues- Typed
keys,values,entries,fromEntries
Async & Error Handling
safe/safeAsync— try/catch as a typedResultvalueretrywith optional exponential backoffmemo/memoAsyncwithmaxSizecache evictiondebounce,throttle,once,deferparallelwith concurrency limit,sequentialtimeout— races a promise against a timer
Date Utilities
timeAgo— human-readable relative time ("3 minutes ago")formatDate— token-based formatting (MMM D, YYYY,HH:mm:ss)addDays,addMonths,addYears,subDays,daysBetweenstartOfDay,endOfDay,startOfWeek,startOfMonthisSameDay,isBefore,isAfterisToday,isYesterday,isWeekend,isThisWeek,isThisYear
Number Utilities
clamp,lerp,normalize— common math for animations and game devroundTo,truncateTo— decimal precision without floating-point surprisesformatBytes—1048576→"1 MB"formatDuration—3661→"1h 1m 1s"toOrdinal—21→"21st"toRoman,formatNumber,randomInt,inRange,average
String Utilities
- Case conversion:
camelCase,snakeCase,kebabCase,pascalCase,titleCase slugify— URL-safe slug generationmask—"4242424242424242"→"************4242"escapeHtml/unescapeHtml— XSS-safe HTML encodingtemplate—"Hello {name}!"interpolationtruncate,excerpt— cut at character or word boundaryrandomId— cryptographically secure viacrypto.getRandomValuesstripHtml,words,isEmpty,countOccurrences
Delay Utilities
delay— simpleawait delay(1000)delaySkippable— resolves early if a condition becomes true (unique to duckkit)delayWithAbort— nativeAbortControllerintegrationrepeat— call a function N times with a delay between each
Encode Utilities
encodeBase64/decodeBase64— standard Base64, Unicode-safeencodeBase64Url/decodeBase64Url— URL-safe Base64, no+,/,=encodeHex/decodeHex— hexadecimal encodingencodeUri/decodeUri— URI component encoding with safe error handlingencodeJson/decodeJson— safe JSON stringify/parse, returnsnullon failure
Validate Utilities
- Format validators:
isEmail,isUrl,isUUID,isIP,isIPv4,isIPv6,isCreditCard,isPhone - Type guards:
isString,isNumber,isBoolean,isArray,isObject,isDate,isFunction - Null checks:
isNull,isUndefined,isNullOrUndefined - Async:
isPromise - Value checks:
isEmpty,isEmptyObject - Enum:
isEnum— checks enum values, not keys
Typed Event Emitter
- Define your event map once — TypeScript enforces payload types on every
emitandon on,off,once,emit,clear- Typos and wrong payload types are caught at compile time, not runtime
Function Composition
pipeline/compose— reusable typed composed functionspipelineAsync/composeAsync— async steps, sync and async freely mixedcurry— partial application with full type inferencetap— side effects inside a pipeline without breaking the chainwhen— conditionally apply a transform
Why duckkit?
Typed groupBy — not any
// lodash — returns Dictionary<User[]>, basically any
const grouped = _.groupBy(users, x => x.country)
// duckkit — returns Record<"GE" | "US", User[]>
const grouped = groupBy(users, x => x.country)
grouped.GE[0].name // string ✅ — full autocomplete, no anydeepClone that preserves Date objects
// JSON trick — everyone uses it, everyone hits this bug
const clone = JSON.parse(JSON.stringify(obj))
clone.createdAt // string ❌ — Date became a string
// duckkit
const clone = deepClone(obj)
clone.createdAt // Date ✅safe — try/catch as a value
// before
let data
try {
data = JSON.parse(raw)
} catch (e) { ... }
// duckkit
const result = safe(() => JSON.parse(raw))
if (result.ok) console.log(result.value) // typed ✅delaySkippable — cancellable wait
// resolves after 3s, or immediately if userClickedSkip becomes true
await delaySkippable(3000, () => userClickedSkip)Typed event emitter
const emitter = createEmitter<{
win: number
spin: void
}>()
emitter.emit('win', 500) // ✅
emitter.emit('win', 'oops') // ❌ TypeScript error
emitter.emit('wiiin', 500) // ❌ typo caught at compile timeImport
// everything
import { groupBy, safe, pipe, clamp, slugify, delay } from 'duckkit'
// or per category — fully tree-shakeable
import { groupBy, partition, sortBy, chunk } from 'duckkit/array'
import { safe, safeAsync, pipe, retry, timeout, once, defer } from 'duckkit/async'
import { formatDate, timeAgo, addDays, startOfDay, isSameDay } from 'duckkit/date'
import { pick, omit, deepMerge, deepClone, flattenObject } from 'duckkit/object'
import { clamp, lerp, roundTo, average, toOrdinal, formatBytes, formatDuration } from 'duckkit/number'
import { slugify, camelCase, escapeHtml, template, mask, excerpt } from 'duckkit/string'
import { delay, delaySkippable, delayWithAbort, repeat } from 'duckkit/delay'
import { createEmitter } from 'duckkit/emitter'
import { pipeline, compose, pipelineAsync, composeAsync, curry, tap, when } from 'duckkit/fn'
import { encodeBase64, decodeBase64, encodeHex, decodeHex, encodeUri, decodeUri, encodeJson, decodeJson } from 'duckkit/encode'
import { isEmail, isUrl, isUUID, isString, isNumber, isEnum, isEmpty } from 'duckkit/validate'Documentation
Full API documentation with examples and edge case notes:
| Module | Docs | |--------|------| | Array | docs/array.md | | Object | docs/object.md | | Async | docs/async.md | | Date | docs/date.md | | Number | docs/number.md | | String | docs/string.md | | Delay | docs/delay.md | | Emitter | docs/emitter.md | | Fn | docs/fn.md | | Encode | docs/encode.md | | Validate | docs/validate.md |
License
MIT — Zura Japoshvili
