Skip to content

Instantly share code, notes, and snippets.

@MagnusThor
Created November 24, 2025 13:03
Show Gist options
  • Select an option

  • Save MagnusThor/c702f015b3ee38a6ab147dd8d9b65744 to your computer and use it in GitHub Desktop.

Select an option

Save MagnusThor/c702f015b3ee38a6ab147dd8d9b65744 to your computer and use it in GitHub Desktop.
/**
* A custom array class that provides additional query-like methods for filtering, mapping, and manipulating arrays.
* @template T - The type of elements in the array.
*/
export class QueryableArray<T> extends Array<T> {
/**
* Skips the specified number of elements and returns the remaining elements.
* @param count - The number of elements to skip.
* @returns A new QueryableArray containing the remaining elements.
*/
skip(count: number): QueryableArray<T> {
const result = QueryableArray.from(this.slice(count));
return result;
}
/**
* Takes the specified number of elements from the start of the array.
* @param count - The number of elements to take.
* @returns A new QueryableArray containing the taken elements.
*/
take(count: number): QueryableArray<T> {
const result = QueryableArray.from(this.slice(0, count));
return result;
}
/**
* Filters the array based on a predicate function.
* @param predicate - A function to test each element.
* @returns A new QueryableArray containing the elements that satisfy the predicate.
*/
where(predicate: (item: T) => boolean): QueryableArray<T> {
const result = QueryableArray.from(this.filter(predicate));
return result;
}
/**
* Projects each element of the array into a new form.
* @template U - The type of the projected elements.
* @param selector - A function to transform each element.
* @returns A new QueryableArray containing the transformed elements.
*/
select<U>(selector: (item: T) => U): QueryableArray<U> {
const result = QueryableArray.from(this.map(selector));
return result;
}
/**
* Returns the first element that satisfies the predicate or the first element if no predicate is provided.
* @param predicate - A function to test each element (optional).
* @returns The first matching element.
* @throws An error if no matching element is found.
*/
first(predicate?: (item: T) => boolean): T {
const item = predicate ? this.find(predicate) : this[0];
if (item === undefined) {
throw new Error('Sequence contains no matching element');
}
return item;
}
/**
* Returns the first element that satisfies the predicate or undefined if no matching element is found.
* @param predicate - A function to test each element (optional).
* @returns The first matching element or undefined.
*/
firstOrDefault(predicate?: (item: T) => boolean): T | undefined {
return predicate ? this.find(predicate) : this[0];
}
/**
* Returns the last element that satisfies the predicate or the last element if no predicate is provided.
* @param predicate - A function to test each element (optional).
* @returns The last matching element.
* @throws An error if no matching element is found.
*/
last(predicate?: (item: T) => boolean): T {
const items = predicate ? this.filter(predicate) : this;
if (items.length === 0) {
throw new Error('Sequence contains no matching element');
}
return items[items.length - 1];
}
/**
* Returns the last element that satisfies the predicate or undefined if no matching element is found.
* @param predicate - A function to test each element (optional).
* @returns The last matching element or undefined.
*/
lastOrDefault(predicate?: (item: T) => boolean): T | undefined {
const items = predicate ? this.filter(predicate) : this;
return items.length > 0 ? items[items.length - 1] : undefined;
}
/**
* Returns the only element that satisfies the predicate or the only element if no predicate is provided.
* @param predicate - A function to test each element (optional).
* @returns The single matching element.
* @throws An error if there is not exactly one matching element.
*/
single(predicate?: (item: T) => boolean): T {
const items = predicate ? this.filter(predicate) : this;
if (items.length !== 1) {
throw new Error('Sequence contains more than one matching element');
}
return items[0];
}
/**
* Returns the only element that satisfies the predicate or undefined if no matching element is found.
* @param predicate - A function to test each element (optional).
* @returns The single matching element or undefined.
*/
singleOrDefault(predicate?: (item: T) => boolean): T | undefined {
const items = predicate ? this.filter(predicate) : this;
return items.length === 1 ? items[0] : undefined;
}
/**
* Determines whether any elements satisfy the predicate or whether the array contains any elements if no predicate is provided.
* @param predicate - A function to test each element (optional).
* @returns True if any elements satisfy the predicate or if the array contains any elements.
*/
any(predicate?: (item: T) => boolean): boolean {
return predicate ? this.some(predicate) : this.length > 0;
}
/**
* Determines whether all elements satisfy the predicate.
* @param predicate - A function to test each element.
* @returns True if all elements satisfy the predicate.
*/
all(predicate: (item: T) => boolean): boolean {
return this.every(predicate);
}
/**
* Counts the number of elements that satisfy the predicate or the total number of elements if no predicate is provided.
* @param predicate - A function to test each element (optional).
* @returns The count of matching elements.
*/
count(predicate?: (item: T) => boolean): number {
return predicate ? this.filter(predicate).length : this.length;
}
/**
* Sorts the elements in ascending order based on a key.
* @template K - The type of the key.
* @param keySelector - A function to extract the key for each element.
* @returns A new QueryableArray containing the sorted elements.
*/
orderBy<K extends keyof T>(keySelector: (item: T) => T[K]): QueryableArray<T> {
const result = new QueryableArray(...this.sort((a, b) => {
if (keySelector(a) < keySelector(b)) return -1;
if (keySelector(a) > keySelector(b)) return 1;
return 0;
}));
return result;
}
/**
* Sorts the elements in descending order based on a key.
* @template K - The type of the key.
* @param keySelector - A function to extract the key for each element.
* @returns A new QueryableArray containing the sorted elements.
*/
orderByDescending<K extends keyof T>(keySelector: (item: T) => T[K]): QueryableArray<T> {
const result = new QueryableArray(...this.sort((a, b) => {
if (keySelector(a) > keySelector(b)) return -1;
if (keySelector(a) < keySelector(b)) return 1;
return 0;
}));
return result;
}
/**
* Groups the elements of the array based on a key.
* @template K - The type of the key.
* @param keySelector - A function to extract the key for each element.
* @returns A Map where the keys are the group keys and the values are QueryableArrays of grouped elements.
*/
groupBy<K>(keySelector: (item: T) => K): Map<K, QueryableArray<T>> {
const map = new Map<K, QueryableArray<T>>();
this.forEach(item => {
const key = keySelector(item);
if (!map.has(key)) {
map.set(key, new QueryableArray<T>());
}
map.get(key)!.push(item);
});
return map;
}
/**
* Removes duplicate elements from the array.
* @returns A new QueryableArray containing only distinct elements.
*/
distinct(): QueryableArray<T> {
const set = new Set(this);
return new QueryableArray(...set);
}
orderByAsc<K>(keySelector: (item: T) => K): QueryableArray<T> {
const result = new QueryableArray(...this.sort((a, b) => {
const keyA = keySelector(a);
const keyB = keySelector(b);
if (keyA < keyB) return -1;
if (keyA > keyB) return 1;
return 0;
}));
return result;
}
/**
* Sorts the elements in descending order based on a key.
* @template K - The type of the key.
* @param keySelector - A function to extract the key for each element.
* @returns A new QueryableArray containing the sorted elements.
*/
orderByDesc<K>(keySelector: (item: T) => K): QueryableArray<T> {
const result = new QueryableArray(...this.sort((a, b) => {
const keyA = keySelector(a);
const keyB = keySelector(b);
if (keyA > keyB) return -1;
if (keyA < keyB) return 1;
return 0;
}));
return result;
}
/**
* Adds a single item to the array.
* @param item - Item to add.
* @returns This QueryableArray (chainable).
*/
add(item: T): this {
this.push(item);
return this;
}
/**
* Adds multiple items to the array.
* @param items - Collection of items to add.
* @returns This QueryableArray (chainable).
*/
addRange(items: Iterable<T>): this {
for (const item of items) {
this.push(item);
}
return this;
}
/**
* Removes the first occurrence of an item from the array.
* @param item - Item to remove.
* @returns True if removed; false if not found.
*/
remove(item: T): boolean {
const index = this.indexOf(item);
if (index === -1) return false;
this.splice(index, 1);
return true;
}
/**
* Removes the item at the specified index.
* @param index - Index of the item to remove.
* @returns True if removed; false if index invalid.
*/
removeAt(index: number): boolean {
if (index < 0 || index >= this.length) return false;
this.splice(index, 1);
return true;
}
/**
* Removes 'count' elements starting at 'start'.
* @param start - Start index.
* @param count - Number of items to remove.
*/
removeRange(start: number, count: number): void {
this.splice(start, count);
}
/**
* Returns a new array excluding values found in 'other'.
* @param other - Items to exclude.
*/
except(other: Iterable<T>): QueryableArray<T> {
const excludeSet = new Set(other);
return new QueryableArray(...this.filter(x => !excludeSet.has(x)));
}
/**
* Returns a new array containing only values also in 'other'.
* @param other - Items to intersect with.
*/
intersect(other: Iterable<T>): QueryableArray<T> {
const setB = new Set(other);
return new QueryableArray(...this.filter(x => setB.has(x)));
}
/**
* Returns a new array with unique items from both arrays.
* @param other - Items to include.
*/
union(other: Iterable<T>): QueryableArray<T> {
return new QueryableArray(...new Set([...this, ...other]));
}
/**
* Flattens one level of nested arrays.
*/
flatten<U>(this: QueryableArray<U[] | U>): QueryableArray<U> {
const flat = ([] as U[]).concat(...(this as unknown as U[][]));
return new QueryableArray<U>(...flat);
}
/**
* Sums all numeric values or values returned by the selector.
*/
sum(selector?: (item: T) => number): number {
if (selector) return this.reduce((acc, x) => acc + selector(x), 0);
return this.reduce((acc, x) => acc + (x as unknown as number), 0);
}
/**
* Average of numeric values.
*/
average(selector?: (item: T) => number): number {
return this.sum(selector) / this.length;
}
/**
* Minimum element or minimum key value.
*/
min(selector?: (item: T) => number): number {
if (selector) return Math.min(...this.map(selector));
return Math.min(...(this as unknown as number[]));
}
/**
* Maximum element or maximum key value.
*/
max(selector?: (item: T) => number): number {
if (selector) return Math.max(...this.map(selector));
return Math.max(...(this as unknown as number[]));
}
/**
* Converts array into a dictionary/map.
*/
toDictionary<K, V>(
keySelector: (item: T) => K,
valueSelector: (item: T) => V
): Map<K, V> {
const map = new Map<K, V>();
this.forEach(item => map.set(keySelector(item), valueSelector(item)));
return map;
}
/**
* Returns a specific page of items.
* @param page - Page number (1-based).
* @param size - Page size (items per page).
*/
paginate(page: number, size: number): QueryableArray<T> {
const start = (page - 1) * size;
const slice = this.slice(start, start + size);
return QueryableArray.from(slice);
}
/**
* Returns distinct items based on a selector.
* Similar to C# LINQ DistinctBy.
* @param selector - A function that selects the comparison key.
*/
uniqueBy<K>(selector: (item: T) => K): QueryableArray<T> {
const seen = new Set<K>();
const result = new QueryableArray<T>();
for (const item of this) {
const key = selector(item);
if (!seen.has(key)) {
seen.add(key);
result.push(item);
}
}
return result;
}
/**
* Sorts by multiple keys in priority order.
* Example:
* arr.sortByMultiple([
* x => x.lastName,
* x => x.firstName,
* x => x.age
* ])
*/
sortByMultiple(selectors: Array<(item: T) => any>): QueryableArray<T> {
const result = new QueryableArray(...this);
result.sort((a, b) => {
for (const selector of selectors) {
const keyA = selector(a);
const keyB = selector(b);
if (keyA < keyB) return -1;
if (keyA > keyB) return 1;
}
return 0;
});
return result;
}
/**
* Pipes the array through a functional transformation.
* Useful for custom operators.
* @param fn - Function receiving this array and returning something.
*/
pipe<U>(fn: (source: QueryableArray<T>) => U): U {
return fn(this);
}
/**
* Creates a new QueryableArray instance from an array-like or iterable object.
* @template T - The type of the elements in the source array.
* @param arrayLike - An array-like or iterable object to convert to a QueryableArray.
* @returns A new QueryableArray instance.
*/
static from<T>(arrayLike: ArrayLike<T> | Iterable<T>): QueryableArray<T> {
return new QueryableArray<T>(...Array.from(arrayLike));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment