duckkit: the utils JS forgot, TypeScript needs, you keep rewriting 🦆
You know the trick. JSON.parse(JSON.stringify(obj)).
Works fine. Until you have a Date in there. Now it's a string. Congrats, you just silently broke something.
That's what pushed me to build duckkit — a TypeScript-first utility library with zero dependencies. Here are the things that don't exist anywhere else.
delaySkippable — a cancellable wait
This one came from game development. You want to wait 3 seconds before continuing, but skip immediately if the user clicks.
await delaySkippable(3000, () => userClickedSkip)
Resolves after 3 seconds normally. Resolves instantly if the condition becomes true. No timers to clear, no Promise races to write manually. Just works.
Also comes with:
await delayWithAbort(2000, abortController.signal)
await repeat(5, 500, i => animate(i))
safe — try/catch as a value
const result = safe(() => JSON.parse(raw))
if (result.ok) {
console.log(result.value) // typed ✅
} else {
console.log(result.error)
}
No try/catch blocks. No let data declared outside. Works with async too:
const result = await safeAsync(() => fetchUser(id))
groupBy that actually types its output
const grouped = groupBy(users, x => x.country)
// Record<"GE" | "US" | "DE", User[]> ✅
Full autocomplete on keys. TypeScript catches typos. The way it should work.
topBy — get the top N items by a field
const top5 = topBy(products, x => x.sales, 5)
You always end up writing .sort(...).slice(0, 5) and it's never clean. One line, typed.
deepClone that preserves Date objects
const clone = deepClone({ name: "Zura", createdAt: new Date() })
clone.createdAt // still a Date, not a string ✅
The JSON trick everyone uses silently converts Dates to strings. This doesn't.
retry and parallel
await retry(() => fetchUser(id), 3, 1000)
await parallel([fetchA, fetchB, fetchC], { concurrency: 2 })
partition with proper types
const [admins, rest] = partition(users, x => x.isAdmin)
// both arrays are User[] ✅
Everything is tree-shakeable
import { groupBy, partition } from 'duckkit/array'
import { safe, retry } from 'duckkit/async'
import { delaySkippable, repeat } from 'duckkit/delay'
import { formatDate, timeAgo } from 'duckkit/date'
Only what you import ends up in the bundle. Zero dependencies. ESM + CJS. Full TypeScript declarations.
Install
npm install duckkit
Still early (v0.1.3) but everything is tested and typed. What helper do you always end up writing from scratch? I might be missing it. 👇
Top comments (0)