yieldless
v0.1.0
Published
Zero-runtime-bloat functional primitives for async TypeScript.
Downloads
107
Readme
Yieldless
Yieldless is a small TypeScript library for people who like the ergonomics of Effect-style code but do not want a custom runtime in the middle of everything.
The library is built around four ideas:
- error handling as simple tuples
- structured concurrency through
AbortController - resource cleanup through native
await using - dependency injection through plain functions
There are no runtime dependencies, and the package is split into subpath exports so callers can pull in only the piece they want.
Status
This repo is intentionally small. The goal is to keep the surface area obvious and let the platform do as much of the work as possible.
Installation
pnpm add yieldlessTypeScript 5.5+ is the target baseline. The package is compiled with isolatedDeclarations enabled.
Modules
yieldless/error
safeTry and safeTrySync turn thrown values into [error, value] tuples.
import { safeTry, safeTrySync, unwrap } from "yieldless/error";
const [readError, body] = await safeTry(fetch("https://example.com"));
if (readError) {
console.error(readError);
}
const parsed = safeTrySync(() => JSON.parse("{\"ok\":true}"));
const value = unwrap(parsed);yieldless/task
runTaskGroup gives you shared cancellation without a separate scheduler or fiber runtime.
import { runTaskGroup } from "yieldless/task";
const result = await runTaskGroup(async (group) => {
const userTask = group.spawn(async (signal) => loadUser(signal));
const auditTask = group.spawn(async (signal) => writeAuditLog(signal));
const user = await userTask;
await auditTask;
return user;
});If one spawned task fails, the group aborts the shared signal, waits for the remaining children to settle, and then rethrows the original failure.
yieldless/resource
acquireResource wraps a value with native async disposal.
import { acquireResource } from "yieldless/resource";
{
await using db = await acquireResource(connect, disconnect);
await db.value.query("select 1");
}The release function runs once when the scope exits.
yieldless/di
inject is just dependency binding for plain functions.
import { inject } from "yieldless/di";
const handler = (
deps: { logger: { info(message: string): void } },
name: string,
) => {
deps.logger.info(`hello ${name}`);
};
const run = inject(handler, {
logger: console,
});
run("world");Design Notes
The package leans on current platform features rather than inventing replacements for them:
Promiseandasync/awaitfor sequencingAbortControllerandAbortSignalfor cancellationAsyncDisposableandSymbol.asyncDisposefor cleanup- ordinary higher-order functions for dependency injection
That keeps the implementation small and makes the failure modes easier to reason about when something goes wrong.
Caveats
SafeResultusesnullas the sentinel value in each tuple slot. If your success value is literallynull, the type system cannot fully discriminate that case.runTaskGroupcan only cancel work that actually respects the passedAbortSignal.await usingrequires runtime support for explicit resource management.
Development
pnpm install
pnpm build
pnpm check