Skip to content

Data<V> bare V branch admits undefined and shadows readonly V[] #1

Closed

Description

@knoan

environment

  • @metreeca/pipe: 0.9.20 (feature/crawl)
  • typescript: 5.9.3
  • node.js: 24.19.0
  • operating system: macOS (Darwin 24.6.0)

steps to reproduce

// 1. `V` does not exclude `undefined`

let calls = 0;

const stream = iterate<undefined | number>(() =>
    ++calls <= 3 ? (calls === 2 ? undefined : calls) : undefined
);

await stream(toArray());  // [1], after 2 generator calls

// 2. bare `V` shadows `readonly V[]` when `V` is itself an array

const arrays = items<number[]>([1, 2]);  // typed `Pipe<number[]>`

await arrays(toArray());  // typed `number[][]`, actually `[1, 2]`

expected behaviour

  1. Data<V> should be built over a V excluding undefined, so the undefined | Data<V> signatures of
    iterate(), flatMap() and crawl() keep their marker distinct from stream values. Streams never carry
    undefined (dropped both as values enter a pipe and as tasks hand them downstream), so admitting it into V
    describes a state that cannot exist.

  2. V and readonly V[] should stay distinguishable when V is itself an array type, so a stream of arrays is
    either expressible or rejected at compile time rather than silently mistyped.

actual behaviour

  1. The bare V branch swallows undefined: for V = undefined | number the type undefined | Data<V> collapses
    to Data<V>, making the value returned by an iterate() generator simultaneously a legal contribution (nothing,
    per the Data documentation) and the end-of-stream marker. The runtime resolves the ambiguity in favour of
    termination (src/feeds/iterate.ts:52), so the probe above stops after the second call and drops item 3.
    flatMap() (src/tasks/flatMap.ts:46) and crawl() (src/tasks/crawl.ts:62, :105) carry the same signature
    and the same collapse. Related: the Data documentation lists undefined as an accepted shape contributing
    nothing, but the union has no undefined member; it is reachable only when V itself admits it.

  2. items<number[]>([1, 2]) type-checks against the bare V branch and declares Pipe<number[]>, but flatten()
    (src/index.core.ts:36) treats every iterable as a sequence, so the array is expanded and the pipe actually
    carries 1 and 2; toArray() is typed number[][] while returning [1, 2]. Conversely, inference resolves
    items([1, 2, 3]) to Pipe<number>, so a stream of arrays cannot be opened from an array literal at all.

workarounds

For (1), keep undefined out of the item type and signal "no items" with an empty array. For (2), wrap array items
in a single-element array (items<number[]>([[1, 2]])), which is sound but at odds with the bare V branch.

notes

Both defects trace to the bare V branch of the union at src/index.ts:67.

Activity

  1. added theissue type on Aug 24, 2026
  2. knoan commented on Aug 24, 2026

    @knoan
    ContributorAuthor

    Proposal: drop undefined from Data-returning signatures

    Rather than reconstructing Data<V> over a V that excludes undefined, remove undefined from the signatures
    built over it:

    • iterate() (src/feeds/iterate.ts:50): () => Awaitable<Data<V>>
    • flatMap() (src/tasks/flatMap.ts:46): (item: V) => Awaitable<Data<R>>
    • crawl() (src/tasks/crawl.ts:62, :109, :110): visitor and traverser likewise

    Streams never carry undefined, so it has no place either among the Data shapes or in the signatures returning
    them. The Data documentation entry listing undefined as a shape contributing nothing (src/index.ts:41) goes
    with them: "contributes nothing" is already expressible by every other shape as an empty batch ([], an empty
    iterable, an empty pipe).

    The termination role is likewise redundant. iterate() currently keys the end of the stream on the undefined
    marker (src/feeds/iterate.ts:52), but an empty batch already terminates it too, one line further down: flatten([])
    yields a done iterator and the first.done branch returns (:60). Keying termination solely on the empty batch
    collapses the two markers into one and removes the ambiguity reported above at its source, with no need for a
    V-excluding-undefined construction.

    In flatMap() and crawl(), where no termination is involved, undefined carries only the "no items" meaning and
    is plainly superseded by the empty batch.

    This addresses defect (1) only; defect (2), the readonly V[] shadowing, remains open.

  3. moved this from Backlog to Active in Packageson Aug 24, 2026
  4. added a commit that references this issue on Aug 25, 2026
    14be083
  5. knoan commented on Aug 25, 2026

    @knoan
    ContributorAuthor

    Solution

    Both defects traced to the bare V branch of the Data<V> union, now dropped: data shapes are batches only
    (readonly (undefined | V)[], Iterable<undefined | V>, AsyncIterable<undefined | V>, Feed<V>), and every shape
    accepts optional entries, dropped as they enter the feed.

    Changes

    • Data<V> no longer admits bare values: feed<number[]>([1, 2]) is rejected at compile time, while
      feed<number[]>([[1, 2]]) and items<number[]>([1, 2]) carry arrays whole
    • optional entries are accepted in every data shape and in task results alike, dropped as they enter the feed, so no
      task or sink ever observes undefined; map and flatMap no longer filter themselves
    • undefined stays a distinct marker where signatures use it: flatMap takes Data<R> rather than
      undefined | Data<R>, with an empty batch contributing nothing
    • collateral renames: Pipe contract to Feed, data feed to feed; chain and merge accept feeds only

    Known limitation

    V is not constrained to exclude undefined: an explicit iterate<undefined | number>(…) still collapses the
    end-of-feed marker with a legal item type. "Feeds never carry undefined" is a documented contract rather than an
    enforced one, and the case is marginal enough not to warrant a V extends {}-style constraint.

    Related

  6. moved this from Active to Done in Packageson Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions