collectype v0.11.0
CollecType is a modern, type-safe collection utility for TypeScript. Effortlessly filter, sort, and transform arrays of any type using a fluent, chainable API. Inspired by the Fluent Interface Design Pattern, CollecType brings expressive, readable, and robust data manipulation to your TypeScript projects.
Its goal: make working with collections as productive and enjoyable as possible, with full type safety and a clean, extensible API.
CollecType runs on Node.js and is available as an NPM package:
npm install collectype
CollecType is built around two main abstractions: the Collection class and a set of “functions” classes (such as BaseFunctions and FullFunctions).
.fn property, which provides a fluent API for filtering, sorting, and transforming your data.BaseFunctions for basic operations, FullFunctions for advanced features, or define your own class extending either to add domain-specific methods.To create a collection, instantiate Collection with your items and the functions class you want to use:
// README Example 1
// index.ts
import { Collection, BaseFunctions } from 'collectype';
import { people } from './data/person';
// Basic collection with core functionality
const collection = new Collection(people, BaseFunctions);
// expect(collection.items.length).toBe(30);
// expect(collection.fn.where((p) => p.age > 20).items.map((p) => p.name)).toContain('George Clooney');
Or use 120 prebuilt filtering methods by injecting FullFunctions — this is a simple form of inversion of control: you pass the functions class as a dependency to the Collection constructor, making the collection’s behavior fully configurable and extensible. This approach allows you to swap, extend, or override the available methods without modifying the Collection itself, promoting flexibility, testability, and clean separation of concerns.
// README Example 2
import { Collection, FullFunctions } from 'collectype';
import { people } from './data/person';
// Collection with advanced functionality
const collection = new Collection(people, FullFunctions);
// expect(collection.items.length).toBe(30);
// expect(collection.fn.numberGreaterOrEqual('age', 18).count).toBe(30);
You can also provide your own functions class to add custom business logic. This lets you create a domain-specific language (DSL) tailored to your business needs. By encapsulating your most common filters and operations as chainable methods, you make your code more readable, predictable, and expressive. This approach bridges the gap between technical code and business language, making intent clear and reducing errors.
// ./person.ts
type Person = {
name: string;
age: number;
gender: 'male' | 'female' | 'other';
single: boolean;
country?: string | undefined;
industry?: string | undefined;
quote?: string | undefined;
hobbies?: string[] | undefined;
};
// README Example 3
import { Collection, BaseFunctions } from 'collectype';
import { Person } from './models/Person';
import { people } from './data/person';
class PersonFunctions extends BaseFunctions<Person> {
// Filter adults (age >= target)
adult(target: number = 18): this {
return this.where((item) => item.age >= target);
}
}
const collection = new Collection(people, PersonFunctions);
// Count how many people are adults
collection.fn.adult().count;
// expect(collection.fn.count).toBe(30);
For full encapsulation, you can create a custom collection class and domain-specific functions. This approach isolates your business logic from the rest of your application, making it easier to reuse, test, and evolve independently. By grouping related filters and operations in dedicated classes, you ensure that your codebase remains organized, maintainable, and clear—especially as your domain grows in complexity. Encapsulation also helps prevent accidental misuse and makes your intent explicit, improving both reliability and onboarding for new developers:
// README Example 4
import { Collection, BaseFunctions, PredicateFn } from 'collectype';
import { GenderEnum, Person } from './models/Person';
import { people } from './data/person';
export class PersonFunctions extends BaseFunctions<Person> {
male(): this {
return this.where((item) => item.gender === GenderEnum.MALE);
}
// Alternative syntax with type for predicate function
female(): this {
const predicate: PredicateFn<Person> = (item) => item.gender === GenderEnum.FEMALE;
return this.where(predicate);
}
adult(target: number = 18): this {
return this.where((item) => item.age >= target);
}
}
const collection = new Collection(people, PersonFunctions);
// Count how many people are adults
collection.fn.adult().count;
// Count how many people are female and adults
collection.fn.female().adult().count;
// Filter how many people are female and adults, then sort them by age
collection.fn.female().adult().sort('age');
// expect(collection.fn.male().count).toBe(14);
// expect(collection.fn.female().count).toBe(16);
// expect(collection.fn.adult().count).toBe(30);
CollecType is powerful enough to infer type, giving you a cleaner and more readable syntax.
import { Collection, BaseFunctions } from 'collectype';
// Explicit type annotation: specify both the item type and the functions class type manually.
const collection1 = new Collection<Person, Constructor<BaseFunctions>>(people, BaseFunctions);
// Inferred type (recommended): TypeScript will infer the correct functions class type from the constructor argument.
const collection2 = new Collection(people, BaseFunctions);
Type requirements:
Collection is always a class constructor for your functions class, typed as Constructor<F>. This ensures type safety and allows CollecType to instantiate your functions class internally.The Constructor type is a utility provided by CollecType:
/**
* Generic constructor type for a class taking any array as argument.
* @template T The instance type returned by the constructor.
*/
export type Constructor<T> = new (items: any[]) => T;
For built-in filtering, sorting, and piping to work out of the box, your items should be plain objects with primitive fields: string, number, boolean, Date, array, or object. All these types are supported by default, with many advanced methods documented below.
If your data includes nested objects, you can still use CollecType, but you may need to write custom filters or predicates to manipulate those fields.
The design is optimized for flat data models, but remains flexible enough to support more complex cases with custom logic.
This design allows you to compose, extend, and reuse collection logic in a type-safe and expressive way.
The BaseFunctions class provides core methods for working with your collections.
Chainable methods (where, sort, page, all, pipe) always return the same BaseFunctions instance (this), with the internal items (this._items) updated by the operation. This allows you to build expressive and composable queries.
The items property gives you the current filtered and/or sorted subset, and count returns its quantity.
Core methods in detail:
all(): Chainable. Returns the same instance with all items.begin(stepName): Chainable. Starts a named step for tracking operations in info.steps. Use with end() to create meaningful step names instead of '_unknown_'.count: Returns the number of items in the current filtered/sorted instance (not chainable).end(): Chainable. Ends the current named step started with begin().info: Returns detailed information about the current state of the collection, including pagination state, sorting state, applied filter steps, and current item count (not chainable).items: Returns the current array of items in the instance, reflecting any applied filters or sorts (not chainable).page(current, perPage?): Chainable. Returns the same instance with internal items paginated to the specified page (1-based indexing, default 20 items per page).pipe('expression'): Chainable. Returns the same instance after applying a sequence of functions, from an expression string.sort(field, direction?): Chainable. Returns the same instance with internal items sorted by the specified field (ascending or descending).where(predicate): Chainable. Returns the same instance with internal items updated to those matching the predicate function.Sorting limitations: Sorting is only supported on primitive fields (string, number, boolean, Date). You cannot sort “out-of-the-box” on fields of type object, set, map, or array.
Pagination restrictions: The
page()method is not available in pipe expressions for architectural consistency. Use direct method chaining instead:collection.fn.where(predicate).page(1, 10)rather thancollection.fn.pipe('where(predicate) | page(1, 10)').
Note:
The items and count properties also exist on the Collection itself, but those always reflect the original, unfiltered data passed to the constructor. In contrast, items, count, and info on the functions instance (fn) reflect the current filtered and/or sorted state after all chained operations. This distinction lets you always access both the raw data and the current query result.
// README Example 5
// src/collections/Person.ts
import { Collection, BaseFunctions } from 'collectype';
import { stringComparisonFactory, numberRangeFactory } from 'collectype';
import { Person } from './models/Person';
export class PersonFunctions extends BaseFunctions<Person> {
stringEquals = stringComparisonFactory<Person, this>(this, 'equals');
numberBetween = numberRangeFactory<Person, this>(this, 'between');
}
export class PersonCollection extends Collection<Person, PersonFunctions> {
constructor(items: Person[]) {
super(items, PersonFunctions);
}
}
// index.ts
import { PersonCollection } from './collections/Person';
import { people } from './data/person';
const collection = new PersonCollection(people);
// Count how many people are named Steve
collection.fn.stringEquals('name', 'Steve').count;
// Count how many people are between 18 and 65 years old
collection.fn.numberBetween('age', 18, 65).count;
// expect(collection.fn.stringEquals('name', 'David Beckham').count).toBe(1);
// expect(collection.fn.numberBetween('age', 18, 65).count).toBe(26);
FullFunctions inherits all the capabilities of BaseFunctions and adds 120 strongly-typed filters for arrays, bigints, booleans, dates, maps, numbers, objects, sets, and strings. All methods are strictly typed and support full TypeScript type inference.
Each method takes the field name as its first argument, and TypeScript autocompletion will guide you based on the field’s type.
This section demonstrates how to leverage advanced filtering, custom domain logic, and method chaining in CollecType. By extending the functions class, you can encapsulate complex business rules and compose them fluently.
Below, we define a PersonFunctions class that adds domain-specific filters (such as male, female, and adult) and a composed method femaleAdultByAge that chains multiple filters and sorts the result.
// README Example 6-7
// src/collections/Person.ts
import { Collection, FullFunctions } from 'collectype';
import { GenderEnum, Person } from './models/Person';
// Custom functions for Person domain
export class PersonFunctions extends FullFunctions<Person> {
// Filter only males
male(): this {
return this.stringEquals('gender', GenderEnum.MALE);
}
// Filter only females
female(): this {
return this.stringEquals('gender', GenderEnum.FEMALE);
}
// Filter adults (age >= target)
adult(target: number = 18): this {
return this.numberGreaterOrEqual('age', target);
}
// Filter olds (age >= target)
old(target: number = 65): this {
return this.numberGreaterOrEqual('age', target);
}
// Filter people who have 'fishing' as a hobby
isFisherman(): this {
return this.arrayIncludes('hobbies', 'fishing');
}
// Filter females who are adults, then sort by age ascending
femaleAdultByAge(): this {
return this.female().adult().sort('age');
}
}
// Custom collection for Person
export class PersonCollection extends Collection<Person, PersonFunctions> {
constructor(items: Person[]) {
super(items, PersonFunctions);
}
}
// index.ts
import { PersonCollection } from './collections/Person';
import { people } from './data/person';
const collection = new PersonCollection(people);
// Count how many people are female and adults, sorted by age
const count = collection.fn.femaleAdultByAge().count;
console.log(count);
// expect(collection.fn.femaleAdultByAge().count).toBe(16);
You can also use the pipe method to apply a sequence of operations from a string expression:
const oldMen = collection.fn.pipe('old(65) | male()').sort('age', 'desc');
console.log(oldMen);
// expect(oldMen.items.map((p) => p.name)).toStrictEqual(['Bill Gates', 'Yannick Noah']);
⚠️ Warning: The
pipemethod evaluates the expression dynamically. If the expression contains a typo, calls a non-existent method, or passes invalid arguments, it will throw a runtime error. Use with caution and prefer direct chaining for type safety whenever possible.
infoThe info property provides comprehensive information about the current state of your collection after all applied operations:
// README Example 8
// src/collections/Person.ts
import { Collection, FullFunctions } from 'collectype';
import { GenderEnum, Person } from './models/Person';
class PersonFunctions extends FullFunctions<Person> {
female(): this {
return this.stringEquals('gender', GenderEnum.FEMALE);
}
adult(target: number = 18): this {
return this.numberGreaterOrEqual('age', target);
}
woman(): this {
return this.begin('Only Women').female().adult().end();
}
}
// Custom collection for Person
export class PersonCollection extends Collection<Person, PersonFunctions> {
constructor(items: Person[]) {
super(items, PersonFunctions);
}
}
// index.ts
import { people } from './data/person';
const collection = new PersonCollection(people);
const result = collection.fn.woman().sort('age', 'asc').page(1, 10);
// expect(collection.fn.woman().sort('age', 'asc').page(1, 10).info).toEqual({
// count: 10,
// steps: ['Only Women'],
// sort: {
// field: 'age',
// direction: 'asc',
// type: 'number',
// },
// page: {
// current: 1,
// perPage: 10,
// startIndex: 0,
// endIndex: 10,
// totalPages: 2,
// totalItems: 16,
// },
// });
The info object contains:
count: Current number of items in the filtered/sorted/paginated resultsteps: Array of named filter steps (custom methods show their names, where() calls show as '_unknown_')sort: Sorting state with field name, direction ('asc'/'desc'), and inferred type ('string'/'number'/'boolean'/'date')page: Pagination state with current page, items per page, start/end indices, total pages, and total items countCollecType is inspired by over a decade of experience working with Ruby on Rails and ActiveRecord (2005–2017). The original prototype for this “collection framework” was built and deployed in a production environment, where it became the backbone of a complex system for KPIs and metrics. The ability to chain methods and create a domain-specific language (DSL) tailored to business needs proved to be a game changer.
When combined with a frontend framework that supports signals or reactivity, this approach enables the creation of powerful, highly responsive applications. After two years of production success, I decided to completely rewrite the framework from the ground up, in my spare time, and share it with the community.
CollecType is my first true open-source contribution, after two decades of learning and benefiting from the incredible work of generous developers who make open source so special. My hope is that this project gives back a little of what I have received and helps others build great things.
At the moment, this project is maintained by a single developer. Contributions are welcome and appreciated. You can find CollecType on GitHub; feel free to open an issue or create a pull request: https://github.com/maduhaime/collectype