Collections
Introduction
The Collection class is a fluent wrapper around a list of items. Use collect to create one, chain transforms, and read values when you are done:
import { collect } from "@bunyad/common";
const collection = collect(["Taylor", "Abigail", null])
.map((name) => (name == null ? "" : name.toUpperCase()))
.reject((name) => name === "");
collection.all();
// ["TAYLOR", "ABIGAIL"]Most methods return a new collection. A few mutate in place (push, put, transform, splice, and similar). Prefer collection methods over converting with all() / toArray() until you hit an HTTP or JSON boundary that needs a plain array.
Numeric index access works on the instance: collection[0] reads the first item.
collect, Collection, and the other helpers below are exported from @bunyad/common. Importing that package also installs collect on globalThis when globals are enabled.
Creating collections
import { Collection, collect } from "@bunyad/common";
const collection = collect([1, 2, 3]);
Collection.make([1, 2, 3]);
Collection.empty();
Collection.times(3, (n) => n * 2); // [2, 4, 6]
Collection.range(1, 5); // [1, 2, 3, 4, 5]
Collection.wrap("a"); // ["a"]
Collection.wrap(["a", "b"]); // ["a", "b"]
Collection.unwrap(collect([1])); // [1]
Collection.fromJson('["a","b"]');Collection.fromOwned(items) wraps an array you already own without copying. Prefer it on hot hydrate / map / filter paths. The public constructor still copies.
import User from "@/Models/User.ts";
const users = await User.query().get();
const names = collect(users).pluck("name");Available methods
For the remainder of this documentation, we discuss each method available on the Collection class. All method signatures are documented on the class itself; the examples below show typical usage.
Method listing
`after()`
Return the item after the given value or predicate. Returns null when there is no next item:
collect([1, 2, 3, 4]).after(2); // 3
collect([1, 2, 3]).after((n) => n > 1); // 3`all()`
Return the underlying array. The array is live — mutating it mutates the collection:
collect([1, 2, 3]).all(); // [1, 2, 3]`average()` / `avg()`
Compute the average of items, or of a key / callback:
collect([1, 1, 2, 4]).avg(); // 2
collect([{ price: 10 }, { price: 20 }]).avg("price"); // 15Empty collections return null.
`before()`
Return the item before the given value or predicate:
collect([1, 2, 3, 4]).before(3); // 2`chunk()`
Break the collection into collections of the given size:
collect([1, 2, 3, 4, 5]).chunk(2).all();
// [Collection[1, 2], Collection[3, 4], Collection[5]]`chunkWhile()`
Chunk while the callback returns truthy for consecutive items; start a new chunk when it returns falsy:
collect([1, 2, 10, 11, 3])
.chunkWhile((n) => n < 10)
.map((c) => c.all())
.all();`collapse()`
Collapse a collection of arrays (or nested collections) one level:
collect([[1, 2], [3, 4]]).collapse().all(); // [1, 2, 3, 4]collapseWithKeys() is an alias of collapse().
`collect()`
Return a new Collection with a copy of the items:
const copy = collect([1, 2]).collect();`combine()`
Combine the collection’s values as keys with another list of values:
collect(["name", "age"]).combine(["Ada", 36]).all();
// [["name", "Ada"], ["age", 36]]`concat()` / `merge()`
Append items from an array or another collection:
collect([1, 2]).concat([3, 4]).all(); // [1, 2, 3, 4]`contains()` / `some()`
Determine if the collection contains a value, or passes a predicate / key comparison:
collect([1, 2, 3]).contains(2); // true
collect([{ id: 1 }]).contains("id", 1); // true
collect([1, 2, 3]).contains((n) => n > 2); // truecontainsStrict() uses ===. doesntContain() / doesntContainStrict() invert the check.
`containsOneItem()` / `containsManyItems()`
collect([1]).containsOneItem(); // true
collect([1, 2]).containsManyItems(); // true
collect([1, 2, 3]).containsOneItem((n) => n > 2); // true`count()`
collect([1, 2, 3]).count(); // 3The instance also exposes length for the same number.
`countBy()`
Count occurrences by value or by a key / callback. Returns a collection of the count numbers:
collect([1, 2, 2, 3]).countBy().all(); // [1, 2, 1]
collect([{ type: "a" }, { type: "a" }, { type: "b" }])
.countBy("type")
.all(); // [2, 1]`crossJoin()`
Cross join the collection’s values among the given arrays:
collect([1, 2]).crossJoin(["a", "b"]).all();
// [[1, "a"], [1, "b"], [2, "a"], [2, "b"]]`dd()` / `dump()`
dump() logs the items and returns the collection. dd() dumps and then throws DdException (or exits in CLI when HTTP dump mode is off):
collect([1, 2, 3]).dump();`diff()` / `diffUsing()`
Return items not present in the other list:
collect([1, 2, 3]).diff([2]).all(); // [1, 3]
collect(["a", "b"]).diffUsing(["A"], (a, b) =>
a.toLowerCase() === b.toLowerCase() ? 0 : 1,
);diffAssoc() / diffAssocUsing() alias diff / diffUsing for list collections.
`diffKeys()` / `diffKeysUsing()`
Keep items whose indexes are not present in the other list or object’s keys:
collect(["a", "b", "c"]).diffKeys(["x"]).all(); // ["b", "c"]`dot()`
Flatten nested objects into dotted values (collection of leaf values):
collect([{ user: { name: "Ada" } }]).dot().all();`duplicates()` / `duplicatesStrict()`
Return one representative item for each duplicated value (or key):
collect([1, 2, 2, 3, 3]).duplicates().all(); // [2, 3]
collect([{ id: 1 }, { id: 1 }, { id: 2 }]).duplicates("id").all();`each()` / `forEach()`
Iterate. Return false from the callback to break:
collect([1, 2, 3]).each((n, i) => {
console.log(n, i);
});`eachSpread()`
When items are arrays, spread them into the callback arguments:
collect([
[1, 2],
[3, 4],
]).eachSpread((a, b) => {
console.log(a, b);
});`ensure()`
Throw if any item fails the type check (typeof string or constructor):
collect([1, 2]).ensure("number");
collect([new Date()]).ensure(Date);`every()`
collect([2, 4, 6]).every((n) => n % 2 === 0); // true`except()` / `only()`
Keep or drop items by index:
collect(["a", "b", "c"]).only([0, 2]).all(); // ["a", "c"]
collect(["a", "b", "c"]).except([1]).all(); // ["a", "c"]`filter()` / `reject()`
collect([1, 2, 3, null, false])
.filter()
.all(); // [1, 2, 3]
collect([1, 2, 3])
.filter((n) => n > 1)
.all(); // [2, 3]
collect([1, 2, 3])
.reject((n) => n > 1)
.all(); // [1]`first()` / `firstOrFail()` / `firstWhere()` / `last()`
collect([1, 2, 3]).first(); // 1
collect([1, 2, 3]).first((n) => n > 1); // 2
collect([1, 2, 3]).first((n) => n > 9, 0); // 0
collect([{ name: "Desk", price: 200 }]).firstWhere("price", 200);
collect([1, 2, 3]).last(); // 3
collect([]).firstOrFail(); // throws`flatMap()`
Map each item to an array or collection, then flatten one level:
collect([1, 2])
.flatMap((n) => [n, n * 10])
.all(); // [1, 10, 2, 20]`flatten()`
Flatten nested arrays / collections to the given depth (default Infinity):
collect([1, [2, [3]]]).flatten().all(); // [1, 2, 3]
collect([1, [2, [3]]]).flatten(1).all(); // [1, 2, [3]]`flip()`
Pair each value with its index:
collect(["a", "b"]).flip().all(); // [["a", 0], ["b", 1]]`forget()`
Remove items by index (mutates):
collect(["a", "b", "c"]).forget(1).all(); // ["a", "c"]`forPage()`
Slice a page of results (1-based page number):
collect([1, 2, 3, 4, 5, 6]).forPage(2, 2).all(); // [3, 4]`fromJson()`
Collection.fromJson("[1,2,3]");`get()` / `getOrPut()` / `put()` / `pull()` / `has()` / `hasAny()` / `hasMany()` / `hasSole()`
Index-based access and mutation:
const c = collect(["a", "b", "c"]);
c.get(1); // "b"
c.has(0, 2); // true
c.hasAny(5, 1); // true
c.put(1, "B");
c.getOrPut(3, () => "d");
c.pull(0); // "a", collection no longer contains it`groupBy()`
Group items by key or callback. Returns a collection of collections (group values):
collect([
{ dept: "Sales", name: "Ada" },
{ dept: "Sales", name: "Lin" },
{ dept: "IT", name: "Sam" },
])
.groupBy("dept")
.map((group) => group.pluck("name").all())
.all();
// [["Ada", "Lin"], ["Sam"]]`implode()` / `join()`
collect(["a", "b", "c"]).implode(", "); // "a, b, c"
collect([{ name: "Ada" }, { name: "Lin" }]).implode(" / ", "name");
collect(["a", "b", "c"]).join(", ", ", and "); // "a, b, and c"`intersect()` / `intersectUsing()` / `intersectByKeys()`
collect([1, 2, 3]).intersect([2, 3, 4]).all(); // [2, 3]
collect(["a", "b", "c"]).intersectByKeys(["x", "y"]).all(); // ["a", "b"]intersectAssoc() / intersectAssocUsing() alias the value-based intersect helpers.
`isEmpty()` / `isNotEmpty()`
collect([]).isEmpty(); // true
collect([1]).isNotEmpty(); // true`keyBy()`
Re-key by attribute, keeping the last item for each key. Returns the values in encounter order:
collect([
{ id: 1, name: "a" },
{ id: 2, name: "b" },
])
.keyBy("id")
.all();`keys()` / `values()`
collect(["a", "b"]).keys().all(); // [0, 1]
collect(["a", "b"]).values().all(); // ["a", "b"]`make()` / `times()` / `range()` / `wrap()` / `unwrap()` / `empty()`
Static constructors — see Creating collections.
`map()` / `mapInto()` / `mapSpread()` / `mapWithKeys()` / `mapToDictionary()` / `mapToGroups()`
collect([1, 2]).map((n) => n * 2).all(); // [2, 4]
class Box {
constructor(public value: number) {}
}
collect([1, 2]).mapInto(Box);
collect([
[1, 2],
[3, 4],
])
.mapSpread((a, b) => a + b)
.all(); // [3, 7]
collect([{ name: "Ada" }])
.mapWithKeys((user) => ({ [user.name]: user }))
.all();`max()` / `min()` / `median()` / `mode()` / `sum()` / `percentage()`
collect([1, 2, 3]).max(); // 3
collect([{ n: 1 }, { n: 9 }]).min("n"); // 1
collect([1, 2, 2, 3]).median(); // 2
collect([1, 1, 2, 2]).mode(); // [1, 2]
collect([1, 2, 3]).sum(); // 6
collect([1, 2, 3, 4]).percentage((n) => n % 2 === 0); // 50`mergeRecursive()`
Merge another list, or shallow-merge an object into each object item:
collect([{ a: 1 }]).mergeRecursive({ b: 2 }).all();
// [{ a: 1, b: 2 }]`multiply()`
Repeat the list times times:
collect([1, 2]).multiply(2).all(); // [1, 2, 1, 2]`nth()`
Take every nth item, optionally starting at an offset:
collect([1, 2, 3, 4, 5, 6]).nth(2).all(); // [1, 3, 5]
collect([1, 2, 3, 4, 5, 6]).nth(2, 1).all(); // [2, 4, 6]`pad()`
Pad to a target length. Negative size pads at the start:
collect([1, 2]).pad(4, 0).all(); // [1, 2, 0, 0]
collect([1, 2]).pad(-4, 0).all(); // [0, 0, 1, 2]`partition()`
Split into [pass, fail] collections:
const [even, odd] = collect([1, 2, 3, 4]).partition((n) => n % 2 === 0);`pipe()` / `pipeInto()` / `pipeThrough()`
const total = collect([1, 2, 3]).pipe((c) => c.sum());
collect([1, 2]).pipeInto(Array); // via ctor(items)
collect([1, 2, 3]).pipeThrough([
(c) => c.filter((n) => n > 1),
(c) => c.map((n) => n * 10),
]);`pluck()`
collect([
{ product_id: "prod-100", name: "Desk" },
{ product_id: "prod-200", name: "Chair" },
])
.pluck("name")
.all(); // ["Desk", "Chair"]With a second argument, Bunyad returns a plain object keyed by that column (Laravel’s associative array). Collection itself stays list-backed, so the keyed form is a Record — the same shape as Arr.pluck(rows, value, key). Duplicate keys keep the last value:
collect([
{ product_id: "prod-100", name: "Desk" },
{ product_id: "prod-200", name: "Chair" },
]).pluck("name", "product_id");
// { "prod-100": "Desk", "prod-200": "Chair" }`pop()` / `shift()` / `push()` / `prepend()` / `unshift()` / `add()`
Mutating stack operations:
const c = collect([1, 2, 3]);
c.pop(); // 3
c.shift(); // 1
c.push(4, 5);
c.prepend(0);
c.add(6);pop(n) / shift(n) with n > 1 return a collection of removed items.
`random()` / `shuffle()`
collect([1, 2, 3, 4]).random(); // one item
collect([1, 2, 3, 4]).random(2); // collection of two
collect([1, 2, 3]).shuffle();`reduce()` / `reduceInto()` / `reduceSpread()` / `reduceWithKeys()`
collect([1, 2, 3]).reduce((carry, n) => carry + n, 0); // 6
collect([1, 2, 3]).reduceInto((bag, n) => {
bag.total += n;
}, { total: 0 });`replace()` / `replaceRecursive()`
Replace items by index from another list:
collect(["a", "b", "c"]).replace(["x"]).all(); // ["x", "b", "c"]`reverse()`
collect([1, 2, 3]).reverse().all(); // [3, 2, 1]`search()`
Return the index of a value or predicate, or false:
collect(["a", "b", "c"]).search("b"); // 1
collect([1, 2, 3]).search((n) => n > 2); // 2`select()`
Pick keys from each object item:
collect([{ id: 1, name: "Ada", role: "admin" }])
.select("id", "name")
.all();
// [{ id: 1, name: "Ada" }]`skip()` / `skipUntil()` / `skipWhile()` / `take()` / `takeUntil()` / `takeWhile()` / `slice()`
collect([1, 2, 3, 4]).skip(2).all(); // [3, 4]
collect([1, 2, 3, 4]).take(2).all(); // [1, 2]
collect([1, 2, 3, 4]).take(-2).all(); // [3, 4]
collect([1, 2, 3, 4]).slice(1, 2).all(); // [2, 3]
collect([1, 2, 3, 4]).skipUntil(3).all(); // [3, 4]
collect([1, 2, 3, 4]).takeWhile((n) => n < 3).all(); // [1, 2]`sliding()`
Sliding windows of size with optional step:
collect([1, 2, 3, 4])
.sliding(2)
.map((c) => c.all())
.all();
// [[1, 2], [2, 3], [3, 4]]`sole()`
Return the only item matching the optional predicate. Throws when zero or many match:
collect([1, 2, 3]).sole((n) => n === 2); // 2`sort()` / `sortDesc()` / `sortBy()` / `sortByDesc()` / `sortByMany()`
collect([3, 1, 2]).sort().all(); // [1, 2, 3]
collect([1, 2, 3]).sortDesc().all(); // [3, 2, 1]
collect([{ name: "Desk", price: 200 }, { name: "Chair", price: 100 }])
.sortBy("price")
.all();
collect(rows).sortByMany([["last", "asc"], ["first", "desc"]]);`sortKeys()` / `sortKeysDesc()` / `sortKeysUsing()`
Reorder by numeric indexes (useful after associative-style operations that still store a list):
collect(["a", "b", "c"]).sortKeysDesc().all(); // ["c", "b", "a"]`splice()`
Remove a portion (and optionally replace), mutating the collection. Returns the removed items:
const c = collect([1, 2, 3, 4]);
const removed = c.splice(1, 2, [9]);
removed.all(); // [2, 3]
c.all(); // [1, 9, 4]`split()` / `splitIn()`
Split into a number of groups:
collect([1, 2, 3, 4, 5, 6])
.split(3)
.map((c) => c.all())
.all();
// [[1, 2], [3, 4], [5, 6]]`tap()`
Run a side-effect callback and return the same collection:
collect([1, 2, 3]).tap((c) => console.log(c.count()));`toArray()` / `toJSON()` / `toJson()` / `toPrettyJson()` / `toBase()`
collect([1, 2]).toArray(); // same as all()
collect([{ toJSON: () => ({ ok: true }) }]).toJSON();
collect([1, 2]).toJson();
collect([1, 2]).toPrettyJson();
collect([1, 2]).toBase(); // new Collection copy`transform()`
Map in place and return this:
collect([1, 2, 3]).transform((n) => n * 2).all(); // [2, 4, 6]`undot()`
Rebuild a nested object from [path, value] pairs when every item is a two-element path tuple:
collect([
["user.name", "Ada"],
["user.role", "admin"],
])
.undot()
.first();
// { user: { name: "Ada", role: "admin" } }`union()`
Concatenate then unique:
collect([1, 2]).union([2, 3]).all(); // [1, 2, 3]`unique()` / `uniqueStrict()`
collect([1, 2, 2, 3]).unique().all(); // [1, 2, 3]
collect([{ id: 1 }, { id: 1 }, { id: 2 }]).unique("id").all();`value()`
Read a key from the first item, or the first item itself:
collect([{ name: "Ada" }]).value("name"); // "Ada"
collect([9]).value(); // 9`when()` / `unless()` / `whenEmpty()` / `whenNotEmpty()` / `unlessEmpty()` / `unlessNotEmpty()`
Conditionally run a callback against the collection. Always returns the same collection instance:
collect([1, 2, 3]).when(true, (c) => {
c.push(4);
});
collect([]).whenEmpty((c) => {
c.push("default");
});`where()` family
Filter by attribute. Two-argument form uses =. Operators: =, ==, ===, !=, <>, !==, <, <=, >, >=.
const products = collect([
{ product: "Desk", price: 200 },
{ product: "Chair", price: 100 },
{ product: "Bookcase", price: 150 },
]);
products.where("price", 100);
products.where("price", ">", 100);
products.whereStrict("price", 100);
products.whereIn("price", [100, 150]);
products.whereNotIn("price", [200]);
products.whereBetween("price", [100, 150]);
products.whereNotBetween("price", [100, 150]);
products.whereNull("optional");
products.whereNotNull("product");
products.whereInstanceOf(Date);whereInStrict / whereNotInStrict compare with ===.
`zip()`
collect(["a", "b"]).zip([1, 2]).all();
// [["a", 1], ["b", 2]]