Non-Empty Collections — Compile-Time Guarantees
In software systems, we frequently operate on collections, strings, or records under the semantic assumption that there is at least one item to process. For example, a validation pipeline must return a list of errors only if at least one error occurred, and a bulk database update requires at least one record to persist.
Standard TypeScript represents these as standard read-only arrays readonly T[], sets, maps,
strings, or records. However, because standard collections are structurally permitted to be empty
([], {}, new Set(), etc.), our codebases become littered with defensive checks, runtime
exceptions, and nullable/undefined handling:
Every access to the head of a potentially empty collection is unsafe. pipelined solves this by
introducing compile-time type-level guarantees that collections, strings, sets, or maps contain at
least one element: Arr.NonEmpty, Rec.NonEmpty, Uniq.NonEmpty, Dict.NonEmpty, and
Str.NonEmpty.
Type Structure
Section titled “Type Structure”Under the hood, non-empty collections leverage TypeScript’s type system:
Arr.NonEmpty<A>: Represented as a structural read-only tuple structurereadonly [A, ...A[]]. Because it extends the standardreadonly A[]interface, it is assignable to standard arrays without conversion.- Branded Non-Empty Types:
Rec.NonEmpty,Uniq.NonEmpty,Dict.NonEmpty, andStr.NonEmptyutilize compile-time phantom brand tags. This guarantees the presence of elements at compile-time while remaining directly assignable to their standard, unbranded counterparts:Uniq.NonEmpty<A>is assignable toReadonlySet<A>Dict.NonEmpty<K, V>is assignable toReadonlyMap<K, V>Rec.NonEmpty<A, K>is assignable toReadonly<Record<K, A>>Str.NonEmptyis assignable tostring
Creating Non-Empty Collections
Section titled “Creating Non-Empty Collections”When receiving collections or strings at the boundaries of your system (such as reading from database queries, parsing API payloads, or processing user inputs), you can refine them using type guards or lift them from known values:
Singletons
Section titled “Singletons”If you already have a value, you can construct a non-empty array, record, set, or map directly:
Refinement (Type Guards)
Section titled “Refinement (Type Guards)”You can refine standard collections or strings using module-level is.nonEmpty type guards. Inside
the conditional blocks, TypeScript automatically narrows the types:
Safe Conversions (Maybe)
Section titled “Safe Conversions (Maybe)”To safely convert potentially empty collections or strings into optional values, use the
namespace-specific from* helpers. They return Some if the collection/string contains
elements/characters, and None if it is empty:
Modifying Non-Empty Arrays
Section titled “Modifying Non-Empty Arrays”Adding elements to standard arrays naturally guarantees a non-empty result. The namespace helpers
Arr.prepend and Arr.append accept standard, potentially empty arrays and return a guaranteed
Arr.NonEmpty:
Transformations and Reductions
Section titled “Transformations and Reductions”Standard library mapping functions operating on standard collections (such as Arr.map or
Rec.map) return standard collections, discarding any compile-time guarantee that the collection is
non-empty.
To transform the elements of a non-empty collection while preserving its non-empty type guarantee,
always use the dedicated mapping helpers under the NonEmpty namespace of the module (e.g.,
Arr.NonEmpty.map, Rec.NonEmpty.map):
When reducing collections down to a single value, you can use the NonEmpty.reduce helpers. Unlike
standard reductions, these do not require an initial accumulator seed value because the collections
are guaranteed to contain at least one element:
When to use Non-Empty Collections
Section titled “When to use Non-Empty Collections”Use Non-Empty collections when:
Section titled “Use Non-Empty collections when:”- Accumulating errors: You want to gather multiple validation failures (like in the
Validationtype) and guarantee that a failure variant contains at least one error. - Requiring payloads: A function or API route demands at least one record (e.g., bulk-updating items or running database transactions).
- Safe head access: You need to safely extract elements without checking for
undefinedor throwing runtime boundaries.
Keep using standard collections when:
Section titled “Keep using standard collections when:”- Absence of elements is valid: A list or record representing filters, tags, or optional configurations is naturally permitted to be empty.
- Interfacing with third-party libraries: Downstream APIs only consume standard collections, and the overhead of checking boundaries is not justified by the application logic.