Collections

Stop writing the same reduce() boilerplate for the 100th time. Get the collection operations JavaScript should have had from day one.

JavaScript Arrays: The Missing Operations

You know the frustration. JavaScript gives you map, filter, reduce... and then leaves you to figure out everything else.

The Daily Struggle

// Need to group users by role? Time to write this... again
const usersByRole = users.reduce((acc, user) => {
  if (!acc[user.role]) {
    acc[user.role] = [];
  }
  acc[user.role].push(user);
  return acc;
}, {});

// Need to split data into chunks? More boilerplate!
const chunks = [];
for (let i = 0; i < data.length; i += chunkSize) {
  chunks.push(data.slice(i, i + chunkSize));
}

// Partition into two groups? Here we go again...
const active = users.filter(u => u.isActive);
const inactive = users.filter(u => !u.isActive);
// Great, we just iterated twice for no reason

// Find the user with the highest score?
const topUser = users.reduce((max, user) =>
  user.score > max.score ? user : max, users[0]);
// Hope users[0] exists!

// Every. Single. Time. You write this boilerplate.
// Your team writes it. Everyone writes it.
// Bugs creep in. Time is wasted. Frustration builds.

The kotlinify-ts Way: Just Call the Method

import { groupBy, chunked, partition, maxBy } from 'kotlinify-ts/collections';

// Group users? One line. Returns type-safe Map.
const usersByRole = groupBy(users, user => user.role); // Map<string, User[]>

// Chunk data? Done.
const chunks = chunked(data, chunkSize);

// Partition? Single pass, destructured result.
const [active, inactive] = partition(users, u => u.isActive);

// Find max? Type-safe and null-safe.
const topUser = maxBy(users, user => user.score);

// Analyze by role with Map iteration
const analysis = Array.from(groupBy(users, u => u.role))
  .map(([role, users]) => ({
    role,
    count: users.length,
    avgScore: users.reduce((sum, u) => sum + u.score, 0) / users.length,
    topPerformer: maxBy(users, u => u.performance)
  }));

// What took 20 lines now takes 5.
// Readable. Testable. Maintainable.

Save Time

Stop reimplementing common patterns. Ship features, not utility functions.

Prevent Bugs

Battle-tested operations that handle edge cases you'd miss in custom code.

Write Less

Express complex data transformations in a few readable lines.

Element Access

Safe and unsafe methods for accessing specific elements with optional predicates.

import { first, firstOrNull, last, lastOrNull, single, singleOrNull } from 'kotlinify-ts/collections';

// Get first element - throws if empty
const firstUser = first(users);
const firstActive = first(users, user => user.active);

// Safe first element - returns undefined if not found
const maybeFirst = firstOrNull(users);
const maybeActive = firstOrNull(users, user => user.active);

// Get last element
const lastOrder = last(orders);
const lastPending = last(orders, order => order.status === 'pending');

// Single element - throws if not exactly one
const onlyAdmin = single(users, user => user.role === 'admin');
const maybeSingle = singleOrNull(users, user => user.id === userId);

// With prototype extensions
import 'kotlinify-ts/collections';

users.first()
  .also(user => console.log('First user:', user.name));

orders
  .last(order => order.total > 1000)
  .let(order => processHighValueOrder(order));

Taking & Dropping

Select or skip elements from the start or end of arrays with flexible conditions.

import { take, takeLast, takeWhile, drop, dropLast, dropWhile } from 'kotlinify-ts/collections';

// Take elements from start
const firstThree = take(items, 3);
const topFive = take(sortedScores, 5);

// Take from end
const recentOrders = takeLast(allOrders, 10);
const lastMessages = takeLast(chatHistory, 20);

// Take while condition is true
const leadingZeros = takeWhile(numbers, n => n === 0);
const beforeError = takeWhile(logs, log => log.level !== 'error');

// Take from end while condition is true
const trailingSpaces = takeLastWhile(chars, c => c === ' ');
const afterSuccess = takeLastWhile(results, r => r.success);

// Drop elements from start
const skipHeader = drop(lines, 1);
const afterWarmup = drop(measurements, 5);

// Drop from end
const withoutFooter = dropLast(lines, 2);
const excludeOutliers = dropLast(sortedValues, 3);

// Drop while condition is true
const afterZeros = dropWhile(data, d => d === 0);
const skipComments = dropWhile(lines, line => line.startsWith('#'));

// Drop from end while condition
const trimTrailing = dropLastWhile(values, v => v == null);

// With prototype extensions
const processed = data
  .dropWhile(item => !item.valid)
  .take(100)
  .map(transform);

const paginated = allItems
  .drop((page - 1) * pageSize)
  .take(pageSize);

Transformations

Transform arrays into maps, grouped collections, and partitioned arrays.

import { flatten, flatMap, zip, unzip, associate, fold, associateBy, associateWith, groupBy, partition } from 'kotlinify-ts/collections';

// Flatten nested arrays
const nested = [[1, 2], [3, 4], [5]];
const flat = flatten(nested); // [1, 2, 3, 4, 5]

// FlatMap - map and flatten in one step
const words = ['hello world', 'foo bar'];
const chars = flatMap(words, word => word.split(''));
// ['h','e','l','l','o',' ','w','o','r','l','d','f','o','o',' ','b','a','r']

// Zip arrays together
const names = ['Alice', 'Bob', 'Charlie'];
const ages = [25, 30, 35];
const people = zip(names, ages); // [['Alice', 25], ['Bob', 30], ['Charlie', 35]]

// Unzip array of pairs
const [firstNames, lastNames] = unzip(fullNames);

// Create Map with custom transform
const userMap = associate(users, user => [user.id, user]);

// Fold (reduce with initial value)
const total = fold(items, 0, (sum, item) => sum + item.price);
const htmlList = fold(tags, '<ul>', (html, tag) => html + `<li>${tag}</li>`) + '</ul>';

// Associate by key - returns Map for type safety
const userLookup = associateBy(users, user => user.id);
const user = userLookup.get(123); // Type-safe Map access

// Associate by key with value transform
const emailToName = associateBy(contacts, c => c.email, c => c.name);
const name = emailToName.get('user@example.com');

// Associate with value - transform values into Map
const permissions = associateWith(roles, role => getPermissionsForRole(role));
const adminPerms = permissions.get('admin');

// Group by key - returns Map<K, T[]>
const usersByRole = groupBy(users, user => user.role);
for (const [role, users] of usersByRole) {
  console.log(`${role}: ${users.length} users`);
}

// Group with value transform
const amountsByCategory = groupBy(transactions, t => t.category, t => t.amount);
const foodTotal = amountsByCategory.get('food')?.reduce((a, b) => a + b, 0);

// Partition - split into two arrays
const [active, inactive] = partition(users, user => user.lastLogin > thirtyDaysAgo);
const [valid, invalid] = partition(data, item => validateItem(item));

Try it yourself:

// Group by and transform
const transactions = [
  { id: 1, category: 'food', amount: 50 },
  { id: 2, category: 'transport', amount: 30 },
  { id: 3, category: 'food', amount: 75 },
  { id: 4, category: 'transport', amount: 20 }
];

const byCategory = groupBy(transactions, t => t.category);

console.log('Groups:', Array.from(byCategory.keys()));

for (const [category, items] of byCategory) {
  const total = items.reduce((sum, t) => sum + t.amount, 0);
  console.log(`${category}: $${total}`);
}

// Zip arrays together
const names = ['Alice', 'Bob', 'Charlie'];
const scores = [95, 87, 92];
const results = zip(names, scores);

console.log('Results:', results);

Reduce & Fold Operations

Accumulate values with flexible reduction strategies including running accumulations.

import { reduce, reduceRight, foldRight, runningFold, runningReduce } from 'kotlinify-ts/collections';

// Reduce without initial value (throws if empty)
const product = reduce([1, 2, 3, 4], (acc, n) => acc * n); // 24
const concatenated = reduce(strings, (acc, str) => acc + ', ' + str);

// Reduce from right
const reversed = reduceRight(chars, (char, acc) => acc + char);
const tree = reduceRight(nodes, (node, acc) => ({ ...node, children: acc }));

// Fold from right with initial value
const folded = foldRight([1, 2, 3], 10, (n, acc) => n + acc); // 16

// Running accumulations - get intermediate results
const runningSum = runningFold([1, 2, 3, 4], 0, (acc, n) => acc + n);
// [0, 1, 3, 6, 10]

const runningMax = runningReduce([3, 1, 4, 1, 5], (acc, n) => Math.max(acc, n));
// [3, 3, 4, 4, 5]

// With prototype extensions
const balance = transactions
  .runningFold(initialBalance, (bal, tx) => bal + tx.amount);

const growth = dailyRevenue
  .runningReduce((acc, revenue) => acc + revenue)
  .map((total, day) => ({ day, total }));

Slice & Distinct

Select specific indices and remove duplicates from arrays.

import { slice, distinct, distinctBy } from 'kotlinify-ts/collections';

// Slice - get elements at specific indices
const selected = slice(items, [0, 2, 4, 6]); // Every other element
const samples = slice(data, randomIndices);

// Slice with Range support
const range = slice(data, { start: 10, endInclusive: 20 }); // indices 10 through 20
const stepped = slice(data, { start: 0, endInclusive: 100, step: 10 }); // every 10th

// Remove duplicates - works with iterables
const unique = distinct([1, 2, 2, 3, 1, 4]); // [1, 2, 3, 4]
const uniqueNames = distinct(users.map(u => u.name));

// Remove duplicates by property - works with iterables
const uniqueUsers = distinctBy(users, user => user.email);
const latestVersions = distinctBy(releases, r => r.major + '.' + r.minor);

Sequence Operations

Process arrays in chunks, windows, and consecutive pairs.

import { chunked, windowed, zipWithNext } from 'kotlinify-ts/collections';

// Split into chunks
const batches = chunked(records, 100);
const pages = chunked(items, pageSize);

// Chunked with transform
const sums = chunked(numbers, 10, chunk => chunk.reduce((a, b) => a + b, 0));
const processed = chunked(records, 100, batch => processBatch(batch));

// Sliding windows
const windows = windowed(prices, 5);
const samples = windowed(dataPoints, 10, 5); // window of 10, step by 5

// Windowed with transform
const movingAvg = windowed(
  prices,
  5,           // window size
  1,           // step
  false,       // no partial windows
  window => window.reduce((a, b) => a + b, 0) / window.length
);

// Partial windows
const allWindows = windowed(data, 3, 1, true); // includes partial at end

// Zip with next - create pairs of consecutive elements
const transitions = zipWithNext(states); // [[s1, s2], [s2, s3], ...]

// Zip with next and transform
const deltas = zipWithNext(values, (prev, curr) => curr - prev);
const changes = zipWithNext(states, (from, to) => ({ from, to }));

Set Operations

Perform mathematical set operations on arrays with automatic deduplication.

import { union, intersect, subtract } from 'kotlinify-ts/collections';

// All set operations work with iterables (arrays, generators, etc.)

// Union - combine unique elements
const allUsers = union(activeUsers, newUsers);
const allTags = union(userTags, systemTags);

// Works with generators
function* gen1() { yield 1; yield 2; }
function* gen2() { yield 2; yield 3; }
const combined = union(gen1(), gen2()); // [1, 2, 3]

// Intersect - common elements only (preserves first iterable's order)
const commonSkills = intersect(requiredSkills, userSkills);
const sharedPermissions = intersect(rolePerms, userPerms);

// Subtract - remove elements from first iterable
const missingFeatures = subtract(requiredFeatures, implementedFeatures);
const nonAdmins = subtract(allUsers, adminUsers);

Aggregations

Calculate sums, find extremes, and verify conditions across collections.

import { count, sum, average, min, max, minOrNull, maxOrNull, sumOf, maxBy, minBy, all, any, none } from 'kotlinify-ts/collections';

// Count elements
const total = count(items);
const activeCount = count(users, user => user.active);

// Numeric aggregations for number arrays
const numbers = [10, 20, 30, 40, 50];
const total = sum(numbers); // 150
const avg = average(numbers); // 30
const lowest = min(numbers); // 10
const highest = max(numbers); // 50

// Safe versions that return null for empty arrays
const safeMin = minOrNull(maybeEmpty); // null if empty
const safeMax = maxOrNull(maybeEmpty); // null if empty

// Sum with selector for object arrays
const totalRevenue = sumOf(orders, order => order.total);
const totalQuantity = sumOf(items, item => item.quantity);

// Find max/min by property
const topScorer = maxBy(players, player => player.score);
const cheapestProduct = minBy(products, product => product.price);

// Boolean predicates
const allValid = all(items, item => item.isValid);
const hasErrors = any(results, result => result.error != null);
const isEmpty = !any(collection); // Check if empty
const noFailures = none(tests, test => test.failed);

// With prototype extensions
const stats = scores
  .filter(s => s > 0)
  .let(valid => ({
    count: valid.count(),
    sum: valid.sum(),
    average: valid.average(),
    min: valid.min(),
    max: valid.max()
  }));

const isComplete = tasks
  .all(task => task.status === 'done');

const needsReview = pullRequests
  .any(pr => pr.reviewers.length === 0);

Real-World Example

Complete examples showing how collection utilities simplify complex data processing.

import { groupBy, distinctBy, chunked, windowed, partition, associateBy, maxBy, sumOf, first, last, flatMap } from 'kotlinify-ts/collections';

// Data processing pipeline
interface Sale {
  id: string;
  userId: string;
  productId: string;
  amount: number;
  date: Date;
  region: string;
}

// Analyze sales data
function analyzeSales(sales: Sale[]) {
  // Group by region and calculate totals - returns Map
  const regionMap = groupBy(sales, sale => sale.region);
  const regionTotals = Array.from(regionMap).map(([region, sales]) => ({
    region,
    total: sumOf(sales, s => s.amount),
    count: sales.length,
    average: sumOf(sales, s => s.amount) / sales.length
  }));

  // Find top performers
  const topSale = maxBy(sales, sale => sale.amount);

  // Identify unique customers
  const uniqueCustomers = distinctBy(sales, sale => sale.userId)
    .map(sale => sale.userId);

  // Process in batches with transform
  const batches = chunked(sales, 50, batch => processBatch(batch));

  // Analyze trends with sliding windows
  const dailySalesMap = groupBy(sales, sale => sale.date.toDateString());
  const dailySales = Array.from(dailySalesMap.values());

  const weeklyTrends = windowed(dailySales, 7, 1, false, week => ({
    start: first(first(week)).date,
    end: first(last(week)).date,
    total: sumOf(flatMap(week, day => day), s => s.amount)
  }));

  return {
    regionTotals,
    topSale,
    uniqueCustomerCount: uniqueCustomers.length,
    batches: Promise.all(batches),
    weeklyTrends
  };
}

// Inventory management
interface Product {
  id: string;
  name: string;
  category: string;
  stock: number;
  reorderPoint: number;
}

function manageInventory(products: Product[]) {
  // Partition into stock levels
  const [inStock, lowStock] = partition(products, p => p.stock > p.reorderPoint);

  // Group by category for reporting - returns Map
  const categoryMap = groupBy(products, p => p.category);
  const byCategory = Array.from(categoryMap).map(([category, items]) => ({
    category,
    totalStock: sumOf(items, p => p.stock),
    needsReorder: items.filter(p => p.stock <= p.reorderPoint)
  }));

  // Create quick lookup map - type-safe access
  const productLookup = associateBy(products, p => p.id);

  // Find critical items
  const critical = products
    .filter(p => p.stock === 0)
    .map(p => productLookup.get(p.id))
    .filter(p => p !== undefined);

  return { inStock, lowStock, byCategory, critical };
}

Comparison with JavaScript

See how kotlinify-ts collections compare to standard JavaScript array methods.

// JavaScript standard methods vs kotlinify-ts

// Finding elements
// JavaScript
const first = users.find(u => u.active); // might be undefined
const last = users.filter(u => u.active).pop(); // awkward for last matching

// kotlinify-ts
const first = first(users, u => u.active); // throws if not found
const last = lastOrNull(users, u => u.active); // safe version

// Grouping
// JavaScript - manual grouping with plain object
const grouped = users.reduce((acc, user) => {
  if (!acc[user.role]) acc[user.role] = [];
  acc[user.role].push(user);
  return acc;
}, {} as Record<string, User[]>); // loses type safety

// kotlinify-ts - returns Map for type safety
const grouped = groupBy(users, user => user.role); // Map<string, User[]>
for (const [role, users] of grouped) { /* fully typed */ }

// Partitioning
// JavaScript - manual partitioning (2 passes!)
const active = users.filter(u => u.active);
const inactive = users.filter(u => !u.active);

// kotlinify-ts - single pass
const [active, inactive] = partition(users, u => u.active);

// Chunking with transform
// JavaScript - manual chunking and mapping
const chunks = [];
for (let i = 0; i < items.length; i += size) {
  chunks.push(processBatch(items.slice(i, i + size)));
}

// kotlinify-ts - declarative with transform
const chunks = chunked(items, size, batch => processBatch(batch));

// Set operations
// JavaScript - using Set
const union = [...new Set([...arr1, ...arr2])];
const intersect = arr1.filter(x => new Set(arr2).has(x));

// kotlinify-ts - works with any iterable
const union = union(arr1, arr2);
const intersect = intersect(arr1, arr2);

API Summary

Element Access

  • first(predicate?) - Get first element
  • firstOrNull(predicate?) - Safe first
  • last(predicate?) - Get last element
  • lastOrNull(predicate?) - Safe last
  • single(predicate?) - Get single element
  • singleOrNull(predicate?) - Safe single

Taking & Dropping

  • take(count) - Take first n elements
  • takeLast(count) - Take last n elements
  • takeWhile(predicate) - Take while true
  • takeLastWhile(predicate) - Take from end while true
  • drop(count) - Skip first n elements
  • dropLast(count) - Skip last n elements
  • dropWhile(predicate) - Skip while true
  • dropLastWhile(predicate) - Skip from end while true

Transformations

  • flatten() - Flatten nested arrays
  • flatMap(transform) - Map and flatten
  • zip(array1, array2) - Combine into pairs
  • unzip(pairs) - Split pairs into arrays
  • associate(transform) - Create Map
  • associateBy(key, value?) - Map by key (returns Map)
  • associateWith(valueSelector) - Map values (returns Map)
  • groupBy(key, value?) - Group by key (returns Map)
  • partition(predicate) - Split in two

Reduce & Fold

  • reduce(operation) - Reduce without initial
  • fold(initial, operation) - Reduce with initial
  • reduceRight(operation) - Reduce from right
  • foldRight(initial, operation) - Fold from right
  • runningReduce(operation) - Running accumulation
  • runningFold(initial, operation) - Running fold

Filtering & Distinct

  • slice(indices | Range) - Get at indices or range
  • distinct() - Remove duplicates (accepts iterables)
  • distinctBy(selector) - Unique by property (accepts iterables)

Sequence Operations

  • chunked(size, transform?) - Fixed-size chunks with optional transform
  • windowed(size, step?, partial?, transform?) - Sliding windows
  • zipWithNext(transform?) - Consecutive pairs with optional transform

Set Operations

  • union(other) - Combine unique (accepts iterables)
  • intersect(other) - Common only (accepts iterables)
  • subtract(other) - Remove elements (accepts iterables)

Aggregations

  • count(predicate?) - Count elements
  • sum() - Sum numbers
  • average() - Average of numbers
  • min(), max() - Min/max values
  • minOrNull(), maxOrNull() - Safe min/max
  • sumOf(selector) - Sum with selector (accepts iterables)
  • maxBy(selector), minBy(selector) - Find by property
  • all(predicate) - Check all match
  • any(predicate?) - Check any match
  • none(predicate) - Check none match

Next Steps