@on-the-ground/quackquack
v0.0.12
Published
Message-based duck typing system focused on behavioral roles instead of structural types.
Maintainers
Readme

❌ Not That Duck (Seriously)
You might have seen another package called duck.
It’s great — but completely different from what we’re doing here.
Here's a quick comparison:
| Feature | duck | quackquack (@on-the-ground/quack) |
| --------------- | -------------------------------------------- | ------------------------------------------------ |
| 📦 Purpose | Assertion matcher for tests | Runtime signature validator |
| 🧠 Focus | Structural object matching | Behavioral contract matching |
| 🔍 Scope | Deep introspection of object contents | Zero introspection – ignores internals |
| 🧪 Typical Use | duck.assertThat(obj, matcher) | expectQuack('(string, number) => boolean')(fn) |
| ⚠️ Fails If | Object is missing properties | Function signature doesn’t match |
| 📘 Mental Model | “Does this object look like this?” | “Can this function play the Role I expect?” |
🎯 Philosophy: Only the Role Matters
quackquack doesn’t care how your function is implemented.
We don’t parse the body.
We don’t read its variables.
We don’t care if it mutates state, opens a socket, or whispers to ducks.
We only care about one thing:
Does it look like it can play the Role?
(As defined by its signature.)
That’s it.
You can define Roles declaratively using our DSL:
const duckFn = expectQuack("(string, number?) => boolean")(fn);Or check full objects at once:
const duckObj = expectDuck({
add: "(number, number) => number",
log: "(string) => void",
})(obj);Under the hood, this creates a runtime contract —
a guarantee that your function (or object) quacks like the Role it's meant to play.
✨ Why Use quackquack?
- ✅ Works at runtime — no TypeScript required
- ✅ Zero dependence on internal object structure
- ✅ Perfect for plugin systems, dynamic registries, adapter validation
- ✅ Supports
strictmode: fail if shape doesn't match exactly - ✅ Bridges the gap between duck typing and role declaration
🧬 Minimalism By Design
“We’re not here to judge how you built it —
we just want to know if it can do the job.”
That’s the quackquack way.
No reflection. No object traversal.
Just plain, declarative, behavioral contracts.
🚀 Quick Start
This library helps you declare Roles via a lightweight DSL, and enforce them at runtime or with decorators.
✅ 1. Mark a function as quackable
const greet = quackable("(name: string) => void")((name) => {
console.log(`Hello, ${name}`);
});✅ 2. Mark a method with the same decorator
class Greeter {
@quackable("(name: string) => void")
greet(name: string) {
console.log(`Hi, ${name}`);
}
}✅ 3. Enforce quack match with expectQuack
const greet = expectQuack("(name: string) => void")(greetFromArgs);If the function wasn't decorated, it'll be wrapped with validation.
If already decorated but mismatched, it throws.
✅ 4. Enforce a whole object with expectDuck
const role = expectDuck({
greet: "(name: string) => void",
add: "(x: number, y: number) => number",
})(obj);Each method will be verified to match the expected Role signature.
✍️ Quack DSL: Writing Quack Signatures
The Quack DSL supports two styles for writing function signatures:
(name: string, age?: number) => void // JavaScript-style
(string, number?) => void // SimplifiedBoth are valid and compiled to the same internal representation.
Note: The DSL is inspired by JavaScript, but it doesn't aim to replicate JS syntax exactly. Its purpose is to express roles, not real function declarations.
🪶 Ultra-Light: No Overhead, No Interference
Once a function or method is quackable (i.e., decorated with @quackable),quackquack will not wrap, proxy, or alter it in any way.
When you expectQuack a quackable function,
It simply compares two precompiled QuackAsts:
- ✅ The expected signature string
- ✅ The given function’s signature
That’s it.
No call-time parsing. No reflection.
Zero overhead. Near-instant validation.
This makes it ideal for:
- Performance-sensitive plugin systems
- Dynamically loaded modules
- High-frequency contract checks
☁️ And Yes, We Can Expect Non-Quackables Too
Not every function needs to be quackable.
You can still use expectQuack or expectDuck
on plain JavaScript functions or objects.
In that case, quackquack will fall back to runtime introspection,
using tools like zod
to validate the shape of parameters, arity, and behavior.
So even if you forgot (or chose not) to decorate it —
you’re still covered.
✅ Decorated? Instant match by signature → Zero overhead
✅ Undecorated? Smart fallback with runtime validation
✅ You? Worry-free API contract checking 🫶
🎭 Role-First Object Orientation
This project is named after Alan Kay — not because it’s directly affiliated with him, but because we deeply respect his philosophy of message passing between objects. Our intention is to carry that spirit forward.
Ironically, however, to truly honor that spirit, we believe we must break free from the habit of treating everything as an object.
Objects only gain real value when they aren’t created arbitrarily. An object deserves to exist only when it can justify its presence — specifically, when it can fulfill a Role.
In this regard, we depart from Alan Kay's assertion that “everything is an object.” We say instead: “Everything begins with a Role.” A Role arises first, born from a concrete need. Then, and only then, does an object emerge — as a candidate that may fulfill that Role.
Messages are not sent to objects, but to Roles. An object is simply a responder — an implementer of the behavior required by the Role.
🧱 Classes Do Not Represent Roles
We often treat classes as mere “bundles of data and methods.” But starting with data and then slapping on some methods quickly leads to unprincipled, trash-bin objects.
Objects are meant to represent high cohesion. But what defines “right” cohesion has always been ambiguous. Now, we propose a clear answer: the right cohesion comes from the right Role.
A Role is essentially a collection of related functions.
For example, the role of a Transaction might include play and rollback.
These functions are grouped not because they share data,
but because they share responsibility for the same behavior.
In this light, cohesion should be driven not by data, but by behavior and responsibility.
⏳ Avoid Bring-Up First Design
If you design objects before you clearly understand the roles they need to fulfill, you fall into the trap of Bring-Up First Design — an accidental architecture shaped by momentum rather than intention.
The most valuable objects and roles only become clear when you wait for genuine needs to emerge. The longer you delay their introduction, the sharper and more robust they become.
🔍 Class vs Role
| Concept | Role | Class | | ------------ | ------------------------------------ | ------------------------------------- | | Declared by | The side that needs the behavior | The side that produces the object | | Purpose | Specify behavioral expectations | Describe internal structure | | Focus | “Can you handle this message?” | “What are you made of?” | | Prerequisite | Must satisfy external demand | Reflects internal design |
While classes describe objects from the inside out, Roles describe them from the outside in — from the perspective of what the world needs.
🧠 Real OOP Is About Behavior, Not Reflection
Real object-oriented programming doesn't ask:
“What are you made of?”
That's reflection — and it has no place in message-driven design.
Instead, real OOP asks:
“I need someone who can play this role. Can you handle it?”
Everything should be designed around behavior and responsibility, not around static structure.
