Dependency Binding
import { Aside } from ‘@astrojs/starlight/components’;
The bindDeps utility enables clean composition boundaries in the fn(args, deps) pattern. It transforms functions from (args, deps) => out into curried form: (deps) => (args) => out.
Why bindDeps?
Section titled “Why bindDeps?”The fn(args, deps) pattern keeps functions testable by making dependencies explicit. However, at composition boundaries (like route handlers, React components, or service entry points), you want to bind dependencies once and call with arguments.
bindDeps bridges this gap:
// Core function: explicit and testableconst notify = (args: { name: string }, deps: { send: SendFn }) => deps.send(args.name);
// At composition root: bind deps onceconst notifySlack = bindDeps(notify)(slackDeps);
// Call sites: clean and simpleawait notifySlack({ name: 'Alice' });Basic Usage
Section titled “Basic Usage”Simple example
Section titled “Simple example”import { bindDeps } from 'awaitly/bind-deps';
// Core function with explicit dependenciesconst greet = (args: { name: string }, deps: { prefix: string }) => `${deps.prefix} ${args.name}`;
// Bind dependencies at composition boundaryconst greetWithHello = bindDeps(greet)({ prefix: 'Hello' });const greetWithHi = bindDeps(greet)({ prefix: 'Hi' });
// Use the bound functionsgreetWithHello({ name: 'Alice' }); // "Hello Alice"greetWithHi({ name: 'Bob' }); // "Hi Bob"Multiple bound functions
Section titled “Multiple bound functions”You can create multiple bound functions from the same base function:
const notify = async ( args: { userId: string; message: string }, deps: { send: SendFn; channel: string }) => { await deps.send(`${deps.channel}:${args.userId}`, args.message); return { sent: true, channel: deps.channel };};
const mockSend: SendFn = async (to, msg) => { console.log(`Sending to ${to}: ${msg}`);};
// Create multiple bound functionsconst notifySlack = bindDeps(notify)({ send: mockSend, channel: 'slack' });const notifyEmail = bindDeps(notify)({ send: mockSend, channel: 'email' });const notifySms = bindDeps(notify)({ send: mockSend, channel: 'sms' });
// All are independentawait notifySlack({ userId: '1', message: 'Hello' });await notifyEmail({ userId: '1', message: 'Hello' });await notifySms({ userId: '1', message: 'Hello' });With Result Types
Section titled “With Result Types”bindDeps works seamlessly with Result and AsyncResult:
import { bindDeps } from 'awaitly/bind-deps';import { ok, err, type AsyncResult } from 'awaitly';
const getUser = async ( args: { id: string }, deps: { db: Map<string, { name: string }> }): AsyncResult<{ name: string }, 'NOT_FOUND'> => { const user = deps.db.get(args.id); return user ? ok(user) : err('NOT_FOUND');};
const db = new Map([['1', { name: 'Alice' }]]);const bound = bindDeps(getUser)({ db });
const result = await bound({ id: '1' });if (result.ok) { console.log(result.value.name); // "Alice"}Testing
Section titled “Testing”The fn(args, deps) pattern makes testing straightforward - just pass mock dependencies:
// Core function is easy to testconst notify = (args: { name: string }, deps: { send: SendFn }) => deps.send(args.name);
// In tests, pass mock dependencies directlyconst mockSend = vi.fn();const result = notify({ name: 'Alice' }, { send: mockSend });
expect(mockSend).toHaveBeenCalledWith('Alice');Framework Integration
Section titled “Framework Integration”Express route handlers
Section titled “Express route handlers”import { bindDeps } from 'awaitly/bind-deps';import express from 'express';
// Core functionconst createOrder = async ( args: { items: OrderItem[] }, deps: { validateOrder: ValidateFn; processPayment: PaymentFn }) => { const validated = await deps.validateOrder(args.items); const payment = await deps.processPayment(validated.total); return { orderId: payment.id };};
// At route boundary: bind deps onceconst app = express();const boundCreateOrder = bindDeps(createOrder)({ validateOrder, processPayment,});
app.post('/orders', async (req, res) => { const result = await boundCreateOrder({ items: req.body.items }); if (result.ok) { res.json(result.value); } else { res.status(400).json({ error: result.error }); }});React components
Section titled “React components”import { bindDeps } from 'awaitly/bind-deps';import { useState } from 'react';
// Core functionconst fetchUser = async ( args: { id: string }, deps: { api: ApiClient }): Promise<User> => { return await deps.api.get(`/users/${args.id}`);};
// At component boundary: bind depsconst api = new ApiClient();const boundFetchUser = bindDeps(fetchUser)({ api });
function UserProfile({ userId }: { userId: string }) { const [user, setUser] = useState<User | null>(null);
useEffect(() => { boundFetchUser({ id: userId }).then(setUser); }, [userId]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;}Next.js server actions
Section titled “Next.js server actions”'use server';
import { bindDeps } from 'awaitly/bind-deps';
// Core functionconst checkout = async ( args: { cartId: string }, deps: { validateCart: ValidateFn; processPayment: PaymentFn }) => { const cart = await deps.validateCart(args.cartId); const payment = await deps.processPayment(cart.total); return { orderId: payment.id };};
// At server action boundary: bind depsconst boundCheckout = bindDeps(checkout)({ validateCart, processPayment,});
export async function handleCheckout(cartId: string) { return await boundCheckout({ cartId });}Type Safety
Section titled “Type Safety”bindDeps preserves all type information:
// TypeScript infers all types automaticallyconst fn = ( args: { id: number; name: string }, deps: { log: boolean }) => args.name;
const bound = bindDeps(fn)({ log: true });
// TypeScript ensures args must match { id: number; name: string }const result = bound({ id: 1, name: 'test' }); // ✅// const result = bound({ id: 1 }); // ❌ Error: missing 'name'Empty Args
Section titled “Empty Args”For functions that don’t need arguments, use Record<string, never>:
const getTimestamp = ( _: Record<string, never>, deps: { now: () => number }) => deps.now();
let time = 1000;const bound = bindDeps(getTimestamp)({ now: () => time++ });
expect(bound({})).toBe(1000);expect(bound({})).toBe(1001);Best Practices
Section titled “Best Practices”- Keep core functions explicit: Write functions in
fn(args, deps)form for testability - Bind at boundaries: Use
bindDepsonly at composition boundaries (routes, components, services) - Preserve types: Let TypeScript infer types - no need for explicit annotations
- Test the core: Test the explicit
fn(args, deps)form with mock dependencies - One bind per boundary: Bind dependencies once per composition boundary, not at every call site
Related
Section titled “Related”- Framework Integration → - Integration patterns for React, Next.js, Express
- Testing Guide → - Testing workflows and functions
- API Reference → - Complete
bindDepsAPI documentation