Skip to content

filter() doesn't narrow on a type guard #2

Closed

Description

@knoan

challenge / use case

A feed of loosely typed items is routinely narrowed with a type guard before the next step reads it, but filter()
accepts only a boolean predicate and reports the input type unchanged:

export function filter<V>(predicate: (item: NoInfer<V>) => Awaitable<boolean>): Task<V>;

A guard such as (value: unknown): value is Record<PropertyKey, unknown> is accepted, as it returns a boolean, but
the task downstream still sees unknown, so the mapper that follows fails to type-check:

pipe(
    (items(scripts))
    (map(node => parse(string(node))))     // unknown
    (filter(isEvent))                      // still unknown
    (map(render))                          // TS2345: unknown is not assignable to Record<PropertyKey, unknown>
);

Observed on @metreeca/flow 0.12.0 while reading schema.org blocks out of a page in ec2u/pipe
(packages/loader/src/events/tasks/read.ts). Array.prototype.filter narrows on a guard, so a pipeline reads as
less typed than the array code it replaces.

proposal

Add a guard overload to filter(), keeping the boolean one as it stands:

export function filter<V, R extends V>(guard: (item: NoInfer<V>) => item is R): Task<V, R>;
export function filter<V>(predicate: (item: NoInfer<V>) => Awaitable<boolean>): Task<V>;

A guard is synchronous by nature (item is R cannot be awaited), so the overload takes no Awaitable. The body is
unchanged: only the reported type differs.

alternatives

  • Splitting the item into a zero-or-one feed and splicing it back, which is what the pipeline above does today:

    (flat(map(node => items(events(parse(string(node)))))))   // events(): unknown → readonly Record[]

    It types correctly and streams, but states a filter as a flat map and needs a helper returning an array where a
    predicate would do.

  • Casting after the filter, which the project's conventions rule out.

notes

when() has the same shape (test: (value: NoInfer<V>) => boolean), and its pass branch would benefit from the
same narrowing; left out of this request to keep it to one task.

Activity

  1. added theissue type on Oct 26, 2026
  2. moved this from Backlog to Active in Packageson Oct 26, 2026
  3. knoan commented on Oct 26, 2026

    @knoan
    ContributorAuthor

    Solution

    Added type guard overloads to filter() and when(), so a guard narrows the item type downstream instead of leaving it unchanged.

    Changes

    • filter(): a type guard narrows the retained items to the type it admits; plain predicates keep the item type
    • when(): a type guard narrows the items routed to the first task; the second task receives them unchanged
    • A guard admitting a type unrelated to the items filters or routes them without narrowing
    • Type tests for both tasks; TypeDoc, tasks module description and README updated

    Related

    • Commit 8068153 on release/0.13, shipping in 0.13.0
  4. moved this from Active to Done in Packageson Oct 26, 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

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions