Skip to content

Commit be0ad00

Browse files
committed
Add partitionArray and refinement support to filterArray
Introduces the partitionArray function for type-safe array partitioning with support for type narrowing via refinements. Enhances filterArray to accept refinement overloads, enabling type-safe filtering (e.g., PositiveInt.is). Adds PredicateWithIndex and RefinementWithIndex types for index-aware predicates and type guards, updates documentation, and cleans up module headers.
1 parent 6b94fca commit be0ad00

22 files changed

Lines changed: 308 additions & 149 deletions

.changeset/gold-carrots-worry.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@evolu/common": minor
3+
---
4+
5+
Added `partitionArray` function and refinement support to `filterArray`.
6+
7+
- New `partitionArray` function partitions arrays returning a tuple of matched/unmatched with type narrowing support
8+
- Enhanced `filterArray` with refinement overloads for type-safe filtering (e.g., `PositiveInt.is`)
9+
- Added `PredicateWithIndex` and `RefinementWithIndex` types for index-aware predicates and type guards
10+
- Improved documentation and cleaned up module headers

packages/common/src/Array.ts

Lines changed: 140 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,44 @@
11
/**
2-
* 🔒 Type-safe array helpers that do not mutate
2+
* Array types, type guards, operations, transformations, accessors, and
3+
* mutations
34
*
4-
* Array types, guards, operations, transformations, accessors, and (rare)
5-
* mutations.
5+
* ### Example
6+
*
7+
* ```ts
8+
* // Types - compile-time guarantee of at least one element
9+
* const _valid: NonEmptyReadonlyArray<number> = [1, 2, 3];
10+
* // ts-expect-error - empty array is not a valid NonEmptyReadonlyArray
11+
* const _invalid: NonEmptyReadonlyArray<number> = [];
12+
*
13+
* // Type guards
14+
* const arr: ReadonlyArray<number> = [1, 2, 3];
15+
* if (isNonEmptyReadonlyArray(arr)) {
16+
* firstInArray(arr);
17+
* }
18+
*
19+
* // Operations
20+
* const appended = appendToArray([1, 2, 3], 4); // [1, 2, 3, 4]
21+
* const prepended = prependToArray([2, 3], 1); // [1, 2, 3]
22+
*
23+
* // Transformations
24+
* const readonly: ReadonlyArray<number> = [1, 2, 3];
25+
* const mapped = mapArray(readonly, (x) => x * 2); // [2, 4, 6]
26+
* const filtered = filterArray(readonly, (x) => x > 1); // [2, 3]
27+
* const deduped = dedupeArray([1, 2, 1, 3, 2]); // [1, 2, 3]
28+
* const [evens, odds] = partitionArray(
29+
* [1, 2, 3, 4, 5],
30+
* (x) => x % 2 === 0,
31+
* ); // [[2, 4], [1, 3, 5]]
32+
*
33+
* // Accessors
34+
* const first = firstInArray(["a", "b", "c"]); // "a"
35+
* const last = lastInArray(["a", "b", "c"]); // "c"
36+
*
37+
* // Mutations
38+
* const mutable: NonEmptyArray<number> = [1, 2, 3];
39+
* shiftArray(mutable); // 1 (guaranteed to exist)
40+
* mutable; // [2, 3]
41+
* ```
642
*
743
* Functions are intentionally data-first to be prepared for the upcoming
844
* JavaScript pipe operator.
@@ -43,43 +79,11 @@
4379
* **Note**: Feel free to use Array instance methods (mutation) if you think
4480
* it's better (performance, local scope, etc.).
4581
*
46-
* ### Example
47-
*
48-
* ```ts
49-
* // Types - compile-time guarantee of at least one element
50-
* const _valid: NonEmptyReadonlyArray<number> = [1, 2, 3];
51-
* // ts-expect-error - empty array is not a valid NonEmptyReadonlyArray
52-
* const _invalid: NonEmptyReadonlyArray<number> = [];
53-
*
54-
* // Guards
55-
* const arr: ReadonlyArray<number> = [1, 2, 3];
56-
* if (isNonEmptyReadonlyArray(arr)) {
57-
* firstInArray(arr);
58-
* }
59-
*
60-
* // Operations
61-
* const appended = appendToArray([1, 2, 3], 4); // [1, 2, 3, 4]
62-
* const prepended = prependToArray([2, 3], 1); // [1, 2, 3]
63-
*
64-
* // Transformations
65-
* const readonly: ReadonlyArray<number> = [1, 2, 3];
66-
* const mapped = mapArray(readonly, (x) => x * 2); // [2, 4, 6]
67-
* const filtered = filterArray(readonly, (x) => x > 1); // [2, 3]
68-
* const deduped = dedupeArray([1, 2, 1, 3, 2]); // [1, 2, 3]
69-
*
70-
* // Accessors
71-
* const first = firstInArray(["a", "b", "c"]); // "a"
72-
* const last = lastInArray(["a", "b", "c"]); // "c"
73-
*
74-
* // Mutations
75-
* const mutable: NonEmptyArray<number> = [1, 2, 3];
76-
* shiftArray(mutable); // 1 (guaranteed to exist)
77-
* mutable; // [2, 3]
78-
* ```
79-
*
8082
* @module
8183
*/
8284

85+
import { PredicateWithIndex, RefinementWithIndex } from "./Types.js";
86+
8387
/**
8488
* An array with at least one element.
8589
*
@@ -175,8 +179,7 @@ export const prependToArray = <T>(
175179
/**
176180
* Maps an array using a mapper function.
177181
*
178-
* Accepts both mutable and readonly arrays. Does not mutate the original array.
179-
* Preserves non-empty type.
182+
* Accepts both mutable and readonly arrays. Preserves non-empty type.
180183
*
181184
* ### Example
182185
*
@@ -202,22 +205,49 @@ export function mapArray<T, U>(
202205
}
203206

204207
/**
205-
* Filters an array using a predicate function, returning a new readonly array.
208+
* Filters an array using a predicate or refinement function, returning a new
209+
* readonly array.
206210
*
207-
* Accepts both mutable and readonly arrays. Does not mutate the original array.
211+
* Accepts both mutable and readonly arrays. When used with a refinement
212+
* function (with `value is Type` syntax), TypeScript will narrow the result
213+
* type to the narrowed type, making it useful for filtering with Evolu Types
214+
* like `PositiveInt.is`.
208215
*
209-
* ### Example
216+
* ### Examples
217+
*
218+
* #### With predicate
210219
*
211220
* ```ts
212221
* filterArray([1, 2, 3, 4, 5], (x) => x % 2 === 0); // [2, 4]
213222
* ```
214223
*
224+
* #### With refinement
225+
*
226+
* ```ts
227+
* const mixed: ReadonlyArray<NonEmptyString | PositiveInt> = [
228+
* NonEmptyString.orThrow("hello"),
229+
* PositiveInt.orThrow(42),
230+
* ];
231+
* const positiveInts = filterArray(mixed, PositiveInt.is);
232+
* // positiveInts: ReadonlyArray<PositiveInt> (narrowed type)
233+
* ```
234+
*
215235
* @category Transformations
216236
*/
217-
export const filterArray = <T>(
237+
export function filterArray<T, S extends T>(
238+
array: ReadonlyArray<T>,
239+
refinement: RefinementWithIndex<T, S>,
240+
): ReadonlyArray<S>;
241+
export function filterArray<T>(
242+
array: ReadonlyArray<T>,
243+
predicate: PredicateWithIndex<T>,
244+
): ReadonlyArray<T>;
245+
export function filterArray<T>(
218246
array: ReadonlyArray<T>,
219-
predicate: (item: T, index: number) => boolean,
220-
): ReadonlyArray<T> => array.filter(predicate) as ReadonlyArray<T>;
247+
predicate: PredicateWithIndex<T>,
248+
): ReadonlyArray<T> {
249+
return array.filter(predicate) as ReadonlyArray<T>;
250+
}
221251

222252
/**
223253
* Returns a new readonly array with duplicate items removed. If `by` is
@@ -272,6 +302,71 @@ export function dedupeArray<T>(
272302
}) as ReadonlyArray<T>;
273303
}
274304

305+
/**
306+
* Partitions an array into two arrays based on a predicate or refinement
307+
* function.
308+
*
309+
* Returns a tuple where the first array contains elements that satisfy the
310+
* predicate, and the second array contains elements that do not. Accepts both
311+
* mutable and readonly arrays.
312+
*
313+
* When used with a refinement function (with `value is Type` syntax),
314+
* TypeScript will narrow the first array to the narrowed type, making it useful
315+
* for filtering with Evolu Types like `PositiveInt.is`.
316+
*
317+
* ### Examples
318+
*
319+
* #### With predicate
320+
*
321+
* ```ts
322+
* const [evens, odds] = partitionArray(
323+
* [1, 2, 3, 4, 5],
324+
* (x) => x % 2 === 0,
325+
* );
326+
* evens; // [2, 4]
327+
* odds; // [1, 3, 5]
328+
* ```
329+
*
330+
* #### With refinement
331+
*
332+
* ```ts
333+
* const mixed: ReadonlyArray<NonEmptyString | PositiveInt> = [
334+
* NonEmptyString.orThrow("hello"),
335+
* PositiveInt.orThrow(42),
336+
* ];
337+
* const [positiveInts, strings] = partitionArray(mixed, PositiveInt.is);
338+
* // positiveInts: ReadonlyArray<PositiveInt> (narrowed type)
339+
* // strings: ReadonlyArray<NonEmptyString> (Exclude<T, PositiveInt>)
340+
* ```
341+
*
342+
* @category Transformations
343+
*/
344+
export function partitionArray<T, S extends T>(
345+
array: ReadonlyArray<T>,
346+
refinement: RefinementWithIndex<T, S>,
347+
): readonly [ReadonlyArray<S>, ReadonlyArray<Exclude<T, S>>];
348+
export function partitionArray<T>(
349+
array: ReadonlyArray<T>,
350+
predicate: PredicateWithIndex<T>,
351+
): readonly [ReadonlyArray<T>, ReadonlyArray<T>];
352+
export function partitionArray<T>(
353+
array: ReadonlyArray<T>,
354+
predicate: PredicateWithIndex<T>,
355+
): readonly [ReadonlyArray<T>, ReadonlyArray<T>] {
356+
const trueArray: Array<T> = [];
357+
const falseArray: Array<T> = [];
358+
359+
for (let i = 0; i < array.length; i++) {
360+
if (predicate(array[i], i)) {
361+
trueArray.push(array[i]);
362+
} else {
363+
falseArray.push(array[i]);
364+
}
365+
}
366+
367+
return [trueArray as ReadonlyArray<T>, falseArray as ReadonlyArray<T>];
368+
}
369+
275370
/**
276371
* Returns the first element of a non-empty array.
277372
*

packages/common/src/Assert.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,3 @@
1-
/**
2-
* 🚨
3-
*
4-
* This module provides assertion utilities to prevent invalid states from
5-
* propagating through the system by halting execution when a condition fails,
6-
* improving reliability and debuggability.
7-
*
8-
* **Warning**: Do not use this instead of {@link Type}. Assertions are intended
9-
* for conditions that are logically guaranteed but not statically known by
10-
* TypeScript, or for catching and signaling developer mistakes eagerly (e.g.,
11-
* invalid configuration).
12-
*
13-
* @module
14-
*/
151
import type { Type } from "./Type.js";
162

173
/**

packages/common/src/Brand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ declare const __brand: unique symbol;
6767
*
6868
* Works with any base type intersected with a `Brand`.
6969
*
70-
* ### Examples
70+
* ### Example
7171
*
7272
* - `IsBranded<string>` -> false
7373
* - `IsBranded<string & Brand<"X">>` -> true

packages/common/src/Cache.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
/**
2-
* 🗄️ Generic cache interface and LRU cache implementation.
3-
*
4-
* @module
5-
*/
6-
71
import { PositiveInt } from "./Type.js";
82

93
/**
@@ -32,7 +26,7 @@ export interface Cache<K, V> {
3226
}
3327

3428
/**
35-
* Creates a Least Recently Used (LRU) cache with a maximum capacity.
29+
* Creates an LRU (least recently used) cache with a maximum capacity.
3630
*
3731
* When the cache reaches capacity, the least recently used entry is evicted.
3832
* Both `get` and `set` operations update the access order.

packages/common/src/Console.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* 📝 Cross-platform console
2+
* Cross-platform console
33
*
44
* Console abstraction for Chrome 123+, Firefox 125+, Safari 18.1+, Node.js
55
* 22.x+, and React Native 0.75+. Includes methods guaranteed to be available in

packages/common/src/Crypto.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
/**
2-
* 🔒
2+
* Cryptographic utilities
3+
*
4+
* Type-safe cryptographic operations including random number generation, SLIP21
5+
* key derivation, XChaCha20-Poly1305 symmetric encryption, PADMÉ padding, and
6+
* timing-safe comparisons.
37
*
48
* @module
59
*/

packages/common/src/Evolu/Internal.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/**
2-
* 🛠️
2+
* Internal Evolu
33
*
44
* ### Example
55
*
66
* ```ts
7-
* import { Evolu } from "@evolu/common/evolu";
7+
* import { createTimestamp } from "@evolu/common/evolu";
88
* ```
99
*
1010
* @module

packages/common/src/Evolu/Public.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* 💾
2+
* Public Evolu
33
*
44
* @module
55
*/

packages/common/src/Order.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
/**
2-
* 🔢
3-
*
4-
* @module
5-
*/
6-
71
/**
82
* Compares two values of type `A` and returns their ordering.
93
*

0 commit comments

Comments
 (0)