Skip to content

Split a signal-held array into two derived signals — the first matches the predicate, the second doesn’t. Both sides re-compute when the source changes; source order is preserved on both sides.

Shipped from the sub-path @whisq/core/collections, not the main entry.

import { partition } from "@whisq/core/collections";
import type { ReadonlySignal } from "@whisq/core";
function partition<T>(
source: () => T[],
predicate: (item: T) => boolean,
): [ReadonlySignal<T[]>, ReadonlySignal<T[]>];
ParamTypeDescription
source() => T[]Reactive accessor for the source array. Typically () => mySignal.value.
predicate(item: T) => booleanReturns true to send the item to the first side (matching), false for the second side (notMatching).

A tuple [matching, notMatching] of ReadonlySignal<T[]>. Each side is an independent computed() — subscribing to one does not subscribe to the other, so an effect reading only pending.value doesn’t re-run when only items in done change.

import { signal, each, ul, li, p, button } from "@whisq/core";
import { partition } from "@whisq/core/collections";
interface Todo { id: string; text: string; done: boolean }
const todos = signal<Todo[]>([
{ id: "1", text: "Learn Whisq", done: false },
{ id: "2", text: "Ship it", done: true },
]);
const [pending, done] = partition(() => todos.value, (t) => !t.done);
// Read either side independently — subscribers don't cross-pollinate.
p(() => `${pending.value.length} left`);
p(() => `${done.value.length} done`);
button({ onclick: () => todos.value = pending.value }, "Clear completed");

You could write:

const pending = computed(() => todos.value.filter((t) => !t.done));
const done = computed(() => todos.value.filter((t) => t.done));

That works and is fine for short cases. partition is a one-line replacement that:

  • Keeps the predicate in one place — the notMatching side is automatically the inverse, so there’s no risk of the two predicates drifting out of sync.
  • Returns a tuple destructured at the call site, matching how partitioning reads in most other languages and libraries.

Reactivity behaviour is identical to two computed()s — both sides use reference-equality on the produced arrays (matching computed() semantics). If you need structural-equality semantics, that’s the caller’s job — wrap the result in your own equality-checked computed().

  • Source order preserved. Both sides receive items in the order they appear in source(). No re-sorting.
  • Reference equality on output. Re-runs only fire when the partitioned array would change in content (per computed()’s default equality).
  • Sub-path import. Lives on @whisq/core/collections to keep the main entry under 5 KB.

See also: computed(), /api/imports/#collections, State Management guide, bindField() → Writing through filtered / partitioned views — pass the source signal to bindField, not the partitioned side, when editing per-row inside a filtered each().

Docs current to v0.1.0-alpha.9 . All releases →