list-fns
This library contains higher order functions for doing common list operations to enable a more declarative style when working with lists. File size is prioritized over performance (several of the functions are O(n^2)); this is not recommended for use with very large datasets.
Example
import { byProperty, get, uniqueByProperty } from "list-fns";
const people = [
{ name: "Jack", age: 44 },
{ name: "Jack", age: 60 },
{ name: "Jane", age: 20 },
];
people
.filter(
(person, index) => index === people.findIndex(p => p.name === person.name)
)
.sort((a, b) => (a.age < b.age ? -1 : a.age > b.age ? 1 : 0))
.map(person => person.name);
people
.filter(uniqueByProperty("name"))
.sort(byProperty("age"))
.map(get("name"));
Install
npm install list-fns
Disclaimer about sorting
This library contains functions to be used with [].sort()
. Always be mindful of the fact that .sort()
and .reverse()
will mutate the original list. If .sort()
is the first method you're calling on a list you should probably clone it first in order to avoid unexpected behavior:
[...list].sort();
list.slice().sort();
[].concat(list).sort();
Functions
Table of contents
by
by: <T>(func: (el: T) => any) => (a: T, b: T) => 0 | 1 | -1
Use with: sort
Sort the elements by func(element)
. Supports sorting by boolean values (elements that are true
first).
[{ a: 2 }, { a: 1 }].sort(by(el => el.a));
Implementation
const by = <T>(func: (el: T) => any) => (a: T, b: T) => {
const A = func(a),
B = func(b);
if (typeof A === "boolean") return A && !B ? -1 : !A && B ? 1 : 0;
return A < B ? -1 : A > B ? 1 : 0;
}
byProperty
byProperty: <TObject extends object, TKey extends keyof TObject>(key: TKey) => (a: TObject, b: TObject) => 0 | 1 | -1
Use with: sort
Sort the elements by element[key]
(can also be an array index). Supports sorting by boolean values (elements that are true
first).
[{ a: 2 }, { a: 1 }].sort(byProperty('a'));
[["a", 2], ["a", 1]].sort(byProperty(1));
Implementation
const byProperty = <TObject extends object, TKey extends keyof TObject>(
key: TKey
) => by<TObject>(get(key))
byValue
byValue: (a: number, b: number) => 0 | 1 | -1
Use with: sort
Sort a list of numbers. This is useful because javascript sorts numbers as string, meaning that [25, 100] results in [100, 25] since "2" is greater than "1"
[100, 25].sort();
[100, 25].sort(byValue);
Implementation
const byValue = (a: number, b: number) => (a < b ? -1 : a > b ? 1 : 0)
countBy
countBy: <T>(func: (el: T) => boolean) => (acc: number, el: T) => number
Use with: reduce
Returns the number of times func
returned true
for the list elements. A number must be passed to the second argument of reduce
. Can be combined with boolean-returning functions like is
, isnt
, propertyIs
or propertyIsOneOf
.
["a", "a", "b"].reduce(countBy(el => el === "a"), 0);
["a", "a", "b"].reduce(countBy(is("a")), 0);
Implementation
const countBy = <T>(func: (el: T) => boolean) => (acc: number, el: T) =>
acc + (func(el) ? 1 : 0)
duplicates
duplicates: (el: unknown, _: number, list: unknown[]) => boolean
Use with: filter
Returns duplicates
[1, 1, 1, 2].filter(duplicates);
Implementation
const duplicates = duplicatesBy(el => el)
duplicatesBy
duplicatesBy: <T>(func: (el: T) => unknown) => (el: T, _: number, list: T[]) => boolean
Use with: filter
Returns all duplicates compared by func(element)
[{ a: 1 }, { a : 1 }, { a: 2 }].filter(duplicatesBy(el => el.a));
Implementation
const duplicatesBy = <T>(func: (el: T) => unknown) => (
el: T,
_: number,
list: T[]
) => numberOfOccurencesBy(list, el, func) > 1
duplicatesByProperty
duplicatesByProperty: <TObject extends object, TKey extends keyof TObject>(key: TKey) => (el: TObject, _: number, list: TObject[]) => boolean
Use with: filter
Returns duplicates compared by element[key]
[{ a: 1 }, { a: 1 }].filter(duplicatesByProperty('a'));
Implementation
const duplicatesByProperty = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey
) => duplicatesBy<TObject>(get(key))
exclude
exclude: <T>(list: T[]) => (el: T) => boolean
Use with: filter
Removes the provided elements from the list
[1, 2, 3, 4].filter(exclude([1, 2]));
Implementation
const exclude = <T>(list: T[]) => (el: T) =>
findIndex(list, a => a === el) === -1
excludeBy
excludeBy: <T>(func: (el: T) => unknown, list: T[]) => (el: T) => boolean
Use with: filter
Removes the provided elements from the list compared by running func
on elements in both lists
[{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]
.filter(excludeBy(el => el.a, [{ a: 1 }, { a: 2 }]));
Implementation
const excludeBy = <T>(func: (el: T) => unknown, list: T[]) => (el: T) =>
findIndex(list, a => func(a) === func(el)) === -1
excludeByProperty
excludeByProperty: <TObject extends object, TKey extends keyof TObject>(key: TKey, list: TObject[]) => (el: TObject) => boolean
Use with: filter
Removes the provided elements from the list compared at key
[{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]
.filter(excludeByProperty('a', [{ a: 1 }, { a: 2 }]));
Implementation
const excludeByProperty = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey,
list: TObject[]
) => excludeBy(get(key), list)
get
get: {
<TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1], TKey3 extends keyof TObject[TKey1][TKey2]>(key1: TKey1, key2: TKey2, key3: TKey3): (obj: TObject) => TObject[TKey1][TKey2][TKey3];
<TObject extends object, TKey1 extends keyof TObject, TKey2 extends keyof TObject[TKey1]>(key1...;
}
Use with map
or filter
Returns element[key]
(can also be an array index). Supports up to three keys of depth.
[{ a: 1 }, { a: 2 }].map(get('a'));
[["a", 1], ["a", 2]].map(get(1));
[{ a: { b: { c: 1 } } }].map(get('a', 'b', 'c'));
Implementation
export function get<
TObject extends object,
TKey1 extends keyof TObject,
TKey2 extends keyof TObject[TKey1],
TKey3 extends keyof TObject[TKey1][TKey2]
>(key1: TKey1, key2?: TKey2, key3?: TKey3) {
return (obj: TObject) => {
if (key3 && key2)
return obj && obj[key1] && obj[key1][key2] && obj[key1][key2][key3];
if (key2) return obj && obj[key1] && obj[key1][key2];
return obj && obj[key1];
};
}
groupBy
groupBy: <K extends string, V>(func: (el: V) => K | undefined) => (acc: Record<K, V[]>, el: V) => Record<K, V[]>
Use with: reduce
Given a key-returning function, returns an object of lists of elements. A second argument must be passed to reduce
. For javascript an empty object is enough. For typescript an object with properties or a type cast is required.
[{ age: 10 }, { age: 80 }].reduce(
groupBy(el => (el.age > 30 ? "old" : "young")),
{ old: [], young: [] }
);
Implementation
const groupBy = <K extends string, V>(
func: (el: V) => K | undefined
) => (acc: Record<K, V[]>, el: V): Record<K, V[]> => {
const groupName = func(el);
if (!groupName) return acc;
const group: V[] = acc[groupName] || [];
return Object.assign({}, acc, { [groupName]: group.concat(el) });
}
groupByProperty
groupByProperty: <K extends keyof V, V extends { [key: string]: any; }>(key: K) => (acc: Record<V[K], V[]>, el: V) => Record<V[K], V[]>
Use with: reduce
Given a property name, returns an object of lists of elements, grouped by the values for that property. A second argument must be passed to reduce
. For javascript an empty object is enough. For typescript an object with properties or a type cast is required.
[{ name: "Jane" }, { name: "John" }].reduce(
groupByProperty("name"),
{}
);
Implementation
const groupByProperty = <
K extends keyof V,
V extends { [key: string]: any }
>(
key: K
) => (acc: Record<V[K], V[]>, el: V): Record<V[K], V[]> => {
const groupName = el[key];
if (!groupName) return acc;
const group: V[] = acc[groupName] || [];
return Object.assign({}, acc, { [groupName]: group.concat(el) });
}
intersection
intersection: <T>(list: T[]) => (el: T) => boolean
Use with: filter
Returns a list of elements that are present in both lists
[1, 2, 3].filter(intersection([2, 3, 4]));
Implementation
const intersection = <T>(list: T[]) => (el: T) =>
findIndex(list, a => a === el) !== -1
intersectionBy
intersectionBy: <T>(func: (el: T) => unknown, list: T[]) => (el: T) => boolean
Use with: filter
Returns a list of elements that are present in both lists compared by running func
on elements in both lists
[{ a: 1 }, { a: 2 }, { a: 3 }]
.filter(intersectionBy(el => el.a, [{ a: 2 }, { a: 3 }, { a: 4 }]));
Implementation
const intersectionBy = <T>(func: (el: T) => unknown, list: T[]) => (
el: T
) => findIndex(list, a => func(a) === func(el)) !== -1
intersectionByProperty
intersectionByProperty: <TObject extends object, TKey extends keyof TObject>(key: TKey, list: TObject[]) => (el: TObject) => boolean
Use with: filter
Returns a list of elements that are present in both lists compared at key
[{ a: 1 }, { a: 2 }, { a: 3 }]
.filter(intersectionByProperty("a", [{ a: 2 }, { a: 3 }, { a: 4 }]));
Implementation
const intersectionByProperty = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey,
list: TObject[]
) => intersectionBy(get(key), list)
is
is: <T>(value: T) => (el: T) => boolean
Use with: find
, filter
Returns true
for elements that are equal to value
[1,2,3].find(is(1));
[1,1,2].filter(is(1));
Implementation
const is = <T>(value: T) => (el: T) => el === value
isBy
isBy: <T, U>(func: (el: T) => U, value: U) => (el: T) => boolean
Use with: find
, filter
Returns true
for elements where func(element)
equals value
[{ a: 1 }, { a: 2 }].find(isBy(el => el.a, 2));
[{ a: 1 }, { a: 2 }].filter(isBy(el => el.a, 2));
Implementation
const isBy = <T, U>(func: (el: T) => U, value: U) => (el: T) =>
func(el) === value
isDefined
isDefined: <T>(x: T | undefined) => x is T
Use with: filter
Removes elements that are undefined
[1, undefined, 2].filter(isDefined);
Implementation
const isDefined = <T>(x: T | undefined): x is T =>
typeof x !== "undefined"
isOneOf
isOneOf: <T>(list: T[]) => (el: T) => boolean
Use with: find
, filter
Alias for intersection
. Returns true
for elements that exist in the provided list
[1,1,2,2,3].filter(isOneOf([2,3]));
Implementation
const isOneOf = intersection
isOneOfBy
isOneOfBy: <T, U>(func: (el: T) => U, list: U[]) => (el: T) => boolean
Use with: find
, filter
Returns true
for elements where func(element)
exists in list
[{ a: 1 }, { a: 2 }, { a: 3 }].find(isOneOfBy(el => el.a, [2, 3]));
[{ a: 1 }, { a: 2 }, { a: 3 }].filter(isOneOfBy(el => el.a, [2, 3]));
Implementation
const isOneOfBy = <T, U>(func: (el: T) => U, list: U[]) => (el: T) =>
findIndex(list, a => a === func(el)) !== -1
isnt
isnt: <T>(value: T) => (el: T) => boolean
Use with: find
, filter
Returns true
for elements that are not equal to value
[1,2,3].find(isnt(1));
[1,2,2].filter(isnt(1));
Implementation
const isnt = <T>(value: T) => (el: T) => el !== value
isntBy
isntBy: <T, U>(func: (el: T) => U, value: U) => (el: T) => boolean
Use with: find
, filter
Returns true
for elements where func(element)
does not equal value
[{ a: 1 }, { a: 2 }].find(isntBy(el => el.a, 2));
[{ a: 1 }, { a: 2 }].filter(isntBy(el => el.a, 2));
Implementation
const isntBy = <T, U>(func: (el: T) => U, value: U) => (el: T) =>
func(el) !== value
isntOneOf
isntOneOf: <T>(list: T[]) => (el: T) => boolean
Use with: find
, filter
Alias for exclude
. Returns true
for elements that do not exist in the provided list
[1,1,2,2,3].filter(isntOneOf([2,3]));
Implementation
const isntOneOf = exclude
isntOneOfBy
isntOneOfBy: <T, U>(func: (el: T) => U, list: U[]) => (el: T) => boolean
Use with: find
, filter
Returns true
for elements where func(element)
exists in list
[{ a: 1 }, { a: 2 }, { a: 3 }].find(isntOneOfBy(el => el.a, [2, 3]));
[{ a: 1 }, { a: 2 }, { a: 3 }].filter(isntOneOfBy(el => el.a, [2, 3]));
Implementation
const isntOneOfBy = <T, U>(func: (el: T) => U, list: U[]) => (el: T) =>
findIndex(list, a => a === func(el)) === -1
max
max: (acc: number, el: number) => number
Use with: reduce
Returns the largest value in the list
[1,2,3,4].reduce(max);
Implementation
const max = (acc: number, el: number) => Math.max(acc, el)
maxBy
maxBy: <T>(func: (el: T) => number) => (acc: T, el: T) => T
Use with: reduce
Returns the largest element by comparing func(element)
[{ a: 1 }, { a: 2 }, { a: 3 }].reduce(maxBy(el => el.a));
Implementation
const maxBy = <T>(func: (el: T) => number) => (acc: T, el: T) =>
func(el) > func(acc) ? el : acc
maxByProperty
maxByProperty: <TObject extends object, TKey extends keyof TObject>(key: TKey) => (acc: TObject, el: TObject) => TObject
Use with: reduce
Returns the largest element by comparing element[key]
[{ a: 1 }, { a: 2 }, { a: 3 }].reduce(maxByProperty("a"));
Implementation
const maxByProperty = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey
) => (acc: TObject, el: TObject) => (el[key] > acc[key] ? el : acc)
min
min: (acc: number, el: number) => number
Use with: reduce
Returns the smallest value in the list
[1,2,3,4].reduce(min);
Implementation
const min = (acc: number, el: number) => Math.min(acc, el)
minBy
minBy: <T>(func: (el: T) => number) => (acc: T, el: T) => T
Use with: reduce
Returns the smallest element by comparing func(element)
[{ a: 1 }, { a: 2 }, { a: 3 }].reduce(minBy(el => el.a));
Implementation
const minBy = <T>(func: (el: T) => number) => (acc: T, el: T) =>
func(el) < func(acc) ? el : acc
minByProperty
minByProperty: <TObject extends object, TKey extends keyof TObject>(key: TKey) => (acc: TObject, el: TObject) => TObject
Use with: reduce
Returns the smallest element by comparing element[key]
[{ a: 1 }, { a: 2 }, { a: 3 }].reduce(minByProperty("a"));
Implementation
const minByProperty = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey
) => (acc: TObject, el: TObject) => (el[key] < acc[key] ? el : acc)
or
or: <T>(fallback: T) => (x: T | undefined) => T
Use with: map
Replaces list elements that are undefined
with fallback
[1, undefined, 2].map(or(0));
Implementation
const or = <T>(fallback: T) => (x: T | undefined): T =>
isDefined(x) ? x : fallback
partition
partition: <T>(func: (el: T) => boolean) => (acc: T[][], el: T) => T[][]
Use with: reduce
Splits the input list into two lists. The first list contains elements for which the given function returned true
, the second contains elements for which the function returned false
.
[{ age: 10 }, { age: 80 }].reduce(partition(el => el.age > 30), []);
Implementation
const partition = <T>(func: (el: T) => boolean) => (
acc: T[][],
el: T
) => {
const a0 = acc[0] || [],
a1 = acc[1] || [];
return func(el) ? [a0.concat(el), a1] : [a0, a1.concat(el)];
}
propertyIs
propertyIs: <TObject extends object, TKey extends keyof TObject>(key: TKey, value: TObject[TKey]) => (el: TObject) => boolean
Use with: find
, filter
Returns true
for elements where element[key]
equals value
[{ a: 1 }, { a: 2 }].find(propertyIs("a", 2));
[{ a: 1 }, { a: 2 }].filter(propertyIs("a", 2))
Implementation
const propertyIs = <TObject extends object, TKey extends keyof TObject>(
key: TKey,
value: TObject[TKey]
) => isBy(get(key), value)
propertyIsOneOf
propertyIsOneOf: <TObject extends object, TKey extends keyof TObject>(key: TKey, list: TObject[TKey][]) => (el: TObject) => boolean
Use with: find
, filter
Returns true
for elements where element[key]
exists in list
[{ a: 1 }, { a: 2 }, { a: 3 }].find(propertyIsOneOf("a", [2, 3]));
[{ a: 1 }, { a: 2 }, { a: 3 }].filter(propertyIsOneOf("a", [2, 3]));
Implementation
const propertyIsOneOf = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey,
list: TObject[TKey][]
) => isOneOfBy(get(key), list)
propertyIsnt
propertyIsnt: <TObject extends object, TKey extends keyof TObject>(key: TKey, value: TObject[TKey]) => (el: TObject) => boolean
Use with: find
, filter
Returns true
for elements where element[key]
does not equal value
[{ a: 1 }, { a: 2 }].find(propertyIsnt("a", 2));
[{ a: 1 }, { a: 2 }].filter(propertyIsnt("a", 2));
Implementation
const propertyIsnt = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey,
value: TObject[TKey]
) => isntBy(get(key), value)
propertyIsntOneOf
propertyIsntOneOf: <TObject extends object, TKey extends keyof TObject>(key: TKey, list: TObject[TKey][]) => (el: TObject) => boolean
Use with: find
, filter
Returns true
for elements where element[key]
exists in list
[{ a: 1 }, { a: 2 }, { a: 3 }].find(propertyIsntOneOf("a", [2, 3]));
[{ a: 1 }, { a: 2 }, { a: 3 }].filter(propertyIsntOneOf("a", [2, 3]));
Implementation
const propertyIsntOneOf = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey,
list: TObject[TKey][]
) => isntOneOfBy(get(key), list)
sum
sum: (acc: number, element: number) => number
Use with: reduce
Sum a list of numbers
[1, 2, 3].reduce(sum);
Implementation
const sum = (acc: number, element: number) => acc + element
sumBy
sumBy: {
<T>(func: (el: T) => number): (acc: number, el: T) => number;
<T>(func: (el: number) => number): (acc: number, el: number) => number;
}
Use with: reduce
Sums the values by applying func
to elements. If the list elements aren't numbers, a number must be passed as the second argument to reduce
.
[{ a: 1 }, { a: 2 }].reduce(sumBy(el => el.a), 0);
[1.5, 2.5].reduce(sumBy(Math.floor));
Implementation
export function sumBy<T>(func: (el: T | number) => number) {
return (acc: number, el: T | number) =>
typeof el === "number" ? func(acc) + func(el) : acc + func(el);
}
sumByProperty
sumByProperty: <TObject extends { [key: string]: number; }, TKey extends keyof TObject>(key: TKey) => (acc: number, el: TObject) => number
Use with: reduce
Sums the values of element[key]
for all elements. A number must be passed to the second argument of reduce
.
[{ a: 1 }, { a: 2 }].reduce(sumByProperty('a'), 0);
Implementation
const sumByProperty = <
TObject extends { [key: string]: number },
TKey extends keyof TObject
>(
key: TKey
) => (acc: number, el: TObject) => acc + el[key]
unique
unique: (el: unknown, index: number, list: unknown[]) => boolean
Use with: filter
Removes duplicates from list
[1,1,1,2].filter(unique);
Implementation
const unique = uniqueBy(el => el)
uniqueBy
uniqueBy: <T>(func: (el: T) => unknown) => (el: T, index: number, list: T[]) => boolean
Use with: filter
Removes duplicates compared by func(element)
[{ a: 1 }, { a : 1 }].filter(uniqueBy(el => el.a));
Implementation
const uniqueBy = <T>(func: (el: T) => unknown) => (
el: T,
index: number,
list: T[]
) => index === findIndex(list, t => func(t) === func(el))
uniqueByProperty
uniqueByProperty: <TObject extends object, TKey extends keyof TObject>(key: TKey) => (el: TObject, index: number, list: TObject[]) => boolean
Use with: filter
Removes duplicates compared by element[key]
[{ a: 1 }, { a: 1 }].filter(uniqueByProperty('a'));
Implementation
const uniqueByProperty = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey
) => uniqueBy<TObject>(get(key))