Small, focused utilities for runtime type handling, lenient value coercion, and mixed sync/async flows in Dart. Hardened for life-critical use: silent data corruption, masked errors, and silent saturations are all defects, not features.
What's in the box:
- Safe value coercion —
letOrNull<T>(input)plus a family oflet{Int,Double,Bool,Num,Uri,DateTime,String,Iterable,List,Set,Map}OrNullhelpers that returnnullon any failure instead of throwing. Rejects silently-unsafe inputs (NaN, infinity, out-of-range doubles) rather than saturating.letIntOrNulluses the runtime-correct safe bound on web (±2^53) versus VM (±2^63).letMapOrNullrejects coerced-key collisions instead of letting one entry overwrite another. - Type-level inspection —
isSubtype<TChild, TParent>(),typeEquality<T1, T2>(), andisNullable<T>()for generic-level checks that aren't otherwise expressible in Dart. FutureOrorchestration —wait,waitF, and theconsec1..consec9family run mixed sync/async work in argument order, witheagerErrorand lifecycle callbacks (onError,onComplete). Stays synchronous when all inputs are synchronous. The original error always reaches the caller — buggy handlers are surfaced throughZone.handleUncaughtErrorbut never mask the incident.onCompleteruns on every exit path.Waiter<T>— a deferred batch of operations you can build up over time and then execute together. Operations are stored as immutableWaiterOperation<T>value objects, which makes the queue auditable and (when callers use top-level functions) sendable across isolates.decodeJsonbStrings— recursively decodes JSON-shaped strings inside a value tree. Handy for Postgresjsonbcolumns that may arrive pre-decoded or as raw JSON depending on the driver. Bounded by amaxDepthparameter (default64) so hostile or pathological nesting can't overflow the stack.- Convenience extensions —
Function.tryCall(safeFunction.apply, but it deliberately does not swallowErrorsubtypes likeStackOverflowErrororAssertionError),Iterable<Enum>.valueOf(case-insensitive enum lookup), andFutureOrExt(isFuture,withMinDuration, etc.).
dart pub add df_type
# or, for a Flutter project:
flutter pub add df_typeimport 'package:df_type/df_type.dart';
void main() async {
// Lenient scalar coercion.
letIntOrNull('42'); // 42
letIntOrNull('not a number'); // null
letIntOrNull(double.nan); // null (never throws, never saturates)
// Nested collection coercion from a JSON string.
letMapOrNull<String, int>('{"a":1,"b":2}'); // {a: 1, b: 2}
// Mixed sync/async, results delivered in the order you passed them in.
final greeting = await consec3<String, int, String, String>(
Future.delayed(const Duration(milliseconds: 10), () => 'hello'),
42,
Future.value('world'),
(a, b, c) => '$a $b $c',
);
print(greeting); // hello 42 world
// Deferred batch of operations via Waiter — `addFn` is the
// closure-friendly shortcut; `add(WaiterOperation(...))` is the
// isolate-portable form.
final waiter = Waiter<String>()
..addFn(() => 'sync result', id: 'a')
..addFn(() async => 'async result', id: 'b');
final results = await waiter.wait();
print(results); // (sync result, async result)
}Waiter stores its queue as immutable WaiterOperation<T> value objects.
Each carries a run function plus an optional id for auditing / logging.
When run is a top-level or static function, the operation (and a list of
them) is safely sendable across an Isolate boundary:
int heavyTask() { /* ... */ }
await Isolate.run(() async {
final w = Waiter<int>(
operations: const [
WaiterOperation(heavyTask, id: 'compute-1'),
WaiterOperation(heavyTask, id: 'compute-2'),
],
);
return (await w.wait()).toList();
});Closures (() => ...) capture their enclosing isolate and cannot cross a
SendPort — that's a Dart runtime restriction, not something the package
imposes. The value-object wrapper exists precisely so the choice between
"sendable" (top-level/static) and "local-only" (closure) is explicit and
inspectable at call sites.
- No silent failures. Misused calls throw
ArgumentErrorin every build mode (no debug-onlyasserts). Coerced-key collisions in maps cause the whole conversion to fail rather than silently overwriting. - The original error always wins. A buggy
onError/onCompletehandler never replaces the underlying incident; its own failure is surfaced viaZone.handleUncaughtErrorso it is still observable but not in the caller's catch block. - Cleanup always runs.
onCompletefires on every exit path, including whenonErroritself throws. - No critical-
Errorabsorption.Function.tryCallswallowsException,TypeError, andNoSuchMethodErroronly —StackOverflow,OutOfMemory,AssertionError, andStateErrorpropagate. - Bounded recursion.
decodeJsonbStringsenforces amaxDepth(default64) so hostile input cannot overflow the stack. - No silent saturation of integers.
letIntOrNullreturnsnulloutside the runtime-appropriate safe bound —±2^63on the VM,±2^53on the JS runtime whereintis double-backed.
The library targets the Dart VM, the JS runtime (Flutter web, dart2js,
dartdevc), and WebAssembly via dart compile wasm / flutter build web --wasm. It has no dart:io or dart:isolate imports under lib/, and
all lib/ sources are pure Dart with no JS-interop or platform conditional
imports. A minimal program exercising the public surface bundles to roughly
100 KB minified via dart2js, or ~85 KB of .wasm + ~13 KB of JS glue via
dart2wasm — both dominated by the SDK runtime rather than this library.
Every top-level binding under lib/ is const or final of an immutable
expression — there is no shared mutable static state, so multiple
isolates can use the package concurrently without interference. A dedicated
test/isolate_safety_test.dart suite
proves this end-to-end on the VM by sending Waiters and operations
through Isolate.run.
🔍 For more information, refer to the API reference.
This is an open-source project, and we warmly welcome contributions from everyone, regardless of experience level. Whether you're a seasoned developer or just starting out, contributing to this project is a fantastic way to learn, share your knowledge, and make a meaningful impact on the community.
- Find us on Discord: Feel free to ask questions and engage with the community here: https://discord.gg/gEQ8y2nfyX.
- Share your ideas: Every perspective matters, and your ideas can spark innovation.
- Help others: Engage with other users by offering advice, solutions, or troubleshooting assistance.
- Report bugs: Help us identify and fix issues to make the project more robust.
- Suggest improvements or new features: Your ideas can help shape the future of the project.
- Help clarify documentation: Good documentation is key to accessibility. You can make it easier for others to get started by improving or expanding our documentation.
- Write articles: Share your knowledge by writing tutorials, guides, or blog posts about your experiences with the project. It's a great way to contribute and help others learn.
No matter how you choose to contribute, your involvement is greatly appreciated and valued!
If you're enjoying this package and find it valuable, consider showing your appreciation with a small donation. Every bit helps in supporting future development. You can donate here: https://www.buymeacoffee.com/dev_cetera
This project is released under the MIT License. See LICENSE for more information.