DEV Community

Zura Japoshvili
Zura Japoshvili

Posted on

duckkit: the utils JS forgot, TypeScript needs, you keep rewriting🦆

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)
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

No try/catch blocks. No let data declared outside. Works with async too:

const result = await safeAsync(() => fetchUser(id))
Enter fullscreen mode Exit fullscreen mode

groupBy that actually types its output

const grouped = groupBy(users, x => x.country)
// Record<"GE" | "US" | "DE", User[]> ✅
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 ✅
Enter fullscreen mode Exit fullscreen mode

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 })
Enter fullscreen mode Exit fullscreen mode

partition with proper types

const [admins, rest] = partition(users, x => x.isAdmin)
// both arrays are User[] ✅
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

Only what you import ends up in the bundle. Zero dependencies. ESM + CJS. Full TypeScript declarations.


Install

npm install duckkit
Enter fullscreen mode Exit fullscreen mode

npm · GitHub · Docs


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)