list-fns
v1.4.0
Published
A collection of utility functions to be used with .map, .filter, .sort and .reduce
Readme
list-fns
This library contains higher order functions that simplify common list operations, similar to what you'd find in lodash or ramda. Unlike these libraries, list-fns is designed specifically to be used with the native array methods.
These functions have not been rigorously tested for performance so they are currently not recommended for use with large datasets.
Example
import { byProperty, get, uniqueByProperty } from "list-fns";
const people = [
{ name: "Jack", age: 44 },
{ name: "Jack", age: 60 },
{ name: "Jane", age: 20 },
];
// Inline implementation:
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); // ["Jane", "Jack"]
// With list functions:
people
.filter(uniqueByProperty("name"))
.sort(byProperty("age"))
.map(get("name")); // ["Jane", "Jack"]Install
npm install list-fnsA note 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
by
by: <T>(func: (el: T) => any) => (a: T, b: T) => 0 | 1 | -1Use 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)); // Returns [{ a: 1 }, { a: 2 }]
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 | -1Use 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')); // Returns [{ a: 1 }, { a: 2 }]
[["a", 2], ["a", 1]].sort(byProperty(1)); // Returns [["a", 1], ["a", 2]]
const byProperty = <TObject extends object, TKey extends keyof TObject>(
key: TKey
) => by<TObject>(get(key))byValue
byValue: (a: number, b: number) => 0 | 1 | -1Use 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(); // Returns [100, 25]
[100, 25].sort(byValue); // Returns [25, 100]
const byValue = (a: number, b: number) => (a < b ? -1 : a > b ? 1 : 0)countBy
countBy: <T>(func: (el: T) => boolean) => (acc: number, el: T) => numberUse 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); // Returns 2
["a", "a", "b"].reduce(countBy(is("a")), 0); // Returns 2
const countBy = <T>(func: (el: T) => boolean) => (acc: number, el: T) =>
acc + (func(el) ? 1 : 0)duplicates
duplicates: (el: unknown, _: number, list: unknown[]) => booleanUse with: filter
Returns duplicates
[1, 1, 1, 2].filter(duplicates); // Returns [1, 1, 1]
const duplicates = duplicatesBy(el => el)duplicatesBy
duplicatesBy: <T>(func: (el: T) => unknown) => (el: T, _: number, list: T[]) => booleanUse with: filter
Returns all duplicates compared by func(element)
[{ a: 1 }, { a : 1 }, { a: 2 }].filter(duplicatesBy(el => el.a)); // Returns [{ a: 1 }, { a: 1 }]
const duplicatesBy = <T>(func: (el: T) => unknown) => (
el: T,
_: number,
list: T[]
) => {
let n = 0;
for (let i = 0; i < list.length; i++) {
if (n >= 2) return true;
if (func(list[i]) === func(el)) n++;
}
return false;
}duplicatesByProperty
duplicatesByProperty: <TObject extends object, TKey extends keyof TObject>(key: TKey) => (el: TObject, _: number, list: TObject[]) => booleanUse with: filter
Returns duplicates compared by element[key]
[{ a: 1 }, { a: 1 }].filter(duplicatesByProperty('a')); // Return [{ a: 1 }, { a: 1 }]
const duplicatesByProperty = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey
) => duplicatesBy<TObject>(get(key))exclude
exclude: <T>(list: T[]) => (el: T) => booleanUse with: filter
Removes the provided elements from the list
[1, 2, 3, 4].filter(exclude([1, 2])); // Returns [3, 4]
const exclude = <T>(list: T[]) => (el: T) =>
findIndex(list, a => a === el) === -1excludeBy
excludeBy: <T>(func: (el: T) => unknown, list: T[]) => (el: T) => booleanUse 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 }]));
// Returns [{ a: 3 }, { a: 4 }]
const excludeBy = <T>(func: (el: T) => unknown, list: T[]) => (el: T) =>
findIndex(list, a => func(a) === func(el)) === -1excludeByProperty
excludeByProperty: <TObject extends object, TKey extends keyof TObject>(key: TKey, list: TObject[]) => (el: TObject) => booleanUse 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 }]));
// Returns [{ a: 3 }, { a: 4 }]
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')); // Returns [1, 2]
[["a", 1], ["a", 2]].map(get(1)); // Returns [1, 2]
[{ a: { b: { c: 1 } } }].map(get('a', 'b', 'c')); // Returns [1]
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 the elements grouped in an object according to the returned keys. 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 may be required.
[{ age: 10 }, { age: 80 }].reduce(
groupBy(el => (el.age > 30 ? "old" : "young")),
{ old: [], young: [] }
); // Returns { old: [{ age: 80 }], young: [{ age: 10 }]}
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;
if (!acc[groupName]) acc[groupName] = [];
acc[groupName].push(el);
return acc;
}groupByMany
groupByMany: <K extends string, V>(func: (el: V) => K[] | undefined) => (acc: Record<K, V[]>, el: V) => Record<K, V[]>Use with: reduce
Given a function func that returns a list of keys, returns an object containing the elements grouped by the returned keys. Unlike the groupBy function, elements can appear several times in this object. Good for grouping objects by properties that are arrays. An empty object must be passed as the second argument to reduce
const b1: B = { items: ["a", "b"] };
const b2: B = { items: ["a"] };
[b1, b2].reduce(groupByMany(b => b.items), {});
// Returns { a: [{ items: ["a", "b"] }, { items: ["a"] }], b: [{ items: ["b"] }] }
const groupByMany = <K extends string, V>(
func: (el: V) => K[] | undefined
) => (acc: Record<K, V[]>, el: V): Record<K, V[]> => {
const groupNames = func(el) || [];
groupNames.forEach(key => {
if (!acc[key]) acc[key] = [];
acc[key].push(el);
});
return acc;
}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 containing the 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 may be required.
[{ name: "Jane" }, { name: "John" }].reduce(
groupByProperty("name"),
{}
); // Returns { Jane: [{ name: "Jane" }], John: [{ name: "John" }] }
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;
if (!acc[groupName]) acc[groupName] = [];
acc[groupName].push(el);
return acc;
}has
has: <TObject extends object, TKey extends keyof TObject>(...keys: TKey[]) => (object: TObject) => object is TObject & HasProperties<TObject, TKey>Use with: find , filter
Returns true for elements where element[key] for all provided keys is defined. This is useful when properties are needed but optional in the element type.
Known limitations: Type inference doesn't always work when list elements have an inferred type.
type Person = { name?: string };
const people: Person[] = [{ name: "John" }, {}];
people.filter(has("name")); // Returns [{ name: "a" }]
const has = <TObject extends object, TKey extends keyof TObject>(
...keys: TKey[]
) => (object: TObject): object is TObject & HasProperties<TObject, TKey> =>
keys.every(key => isDefined(object[key]))intersection
intersection: <T>(list: T[]) => (el: T) => booleanUse with: filter
Returns a list of elements that are present in both lists
[1, 2, 3].filter(intersection([2, 3, 4])); // Returns [2, 3]
const intersection = <T>(list: T[]) => (el: T) =>
findIndex(list, a => a === el) !== -1intersectionBy
intersectionBy: <T>(func: (el: T) => unknown, list: T[]) => (el: T) => booleanUse 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 }]));
// Returns [{ a: 2 }, { a: 3 }]
const intersectionBy = <T>(func: (el: T) => unknown, list: T[]) => (
el: T
) => findIndex(list, a => func(a) === func(el)) !== -1intersectionByProperty
intersectionByProperty: <TObject extends object, TKey extends keyof TObject>(key: TKey, list: TObject[]) => (el: TObject) => booleanUse 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 }]));
// Returns [{ a: 2 }, { a: 3 }]
const intersectionByProperty = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey,
list: TObject[]
) => intersectionBy(get(key), list)is
is: <T>(value: T) => (el: T) => booleanUse with: find , filter
Returns true for elements that are equal to value
[1,2,3].find(is(1)); // Returns 1
[1,1,2].filter(is(1)); // Returns [1, 1]
const is = <T>(value: T) => (el: T) => el === valueisBy
isBy: <T, U>(func: (el: T) => U, value: U) => (el: T) => booleanUse with: find , filter
Returns true for elements where func(element) equals value
[{ a: 1 }, { a: 2 }].find(isBy(el => el.a, 2)); // Returns { a: 2 }
[{ a: 1 }, { a: 2 }].filter(isBy(el => el.a, 2)); // Returns [{ a: 2 }]
const isBy = <T, U>(func: (el: T) => U, value: U) => (el: T) =>
func(el) === valueisDefined
isDefined: <T>(x: T) => x is NonNullable<T>Use with: filter
Remove elements that are undefined or null
[1, null, undefined, 2].filter(isDefined); // Returns [1, 2]
const isDefined = <T>(x: T): x is NonNullable<T> =>
x !== undefined && x !== nullisOneOf
isOneOf: <T>(list: T[]) => (el: T) => booleanUse 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])); // Returns [2, 2, 3]
const isOneOf = intersectionisOneOfBy
isOneOfBy: <T, U>(func: (el: T) => U, list: U[]) => (el: T) => booleanUse 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]));
// ^ Returns { a: 2 }
[{ a: 1 }, { a: 2 }, { a: 3 }].filter(isOneOfBy(el => el.a, [2, 3]));
// ^ Returns [{ a: 2 }, { a: 3 }]
const isOneOfBy = <T, U>(func: (el: T) => U, list: U[]) => (el: T) =>
findIndex(list, a => a === func(el)) !== -1isnt
isnt: <T>(value: T) => (el: T) => booleanUse with: find , filter
Returns true for elements that are not equal to value
[1,2,3].find(isnt(1)); // Returns 2
[1,2,2].filter(isnt(1)); // Returns [2,2]
const isnt = <T>(value: T) => (el: T) => el !== valueisntBy
isntBy: <T, U>(func: (el: T) => U, value: U) => (el: T) => booleanUse with: find , filter
Returns true for elements where func(element) does not equal value
[{ a: 1 }, { a: 2 }].find(isntBy(el => el.a, 2)); // Returns { a: 1 }
[{ a: 1 }, { a: 2 }].filter(isntBy(el => el.a, 2)); // Returns [{ a: 1 }]
const isntBy = <T, U>(func: (el: T) => U, value: U) => (el: T) =>
func(el) !== valueisntOneOf
isntOneOf: <T>(list: T[]) => (el: T) => booleanUse 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])); // Returns [1, 1]
const isntOneOf = excludeisntOneOfBy
isntOneOfBy: <T, U>(func: (el: T) => U, list: U[]) => (el: T) => booleanUse 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]));
// ^ Returns { a: 1 }
[{ a: 1 }, { a: 2 }, { a: 3 }].filter(isntOneOfBy(el => el.a, [2, 3]));
// ^ Returns [{ a: 1 }]
const isntOneOfBy = <T, U>(func: (el: T) => U, list: U[]) => (el: T) =>
findIndex(list, a => a === func(el)) === -1max
max: (acc: number, el: number) => numberUse with: reduce
Returns the largest value in the list
[1,2,3,4].reduce(max); // Returns 4
const max = (acc: number, el: number) => Math.max(acc, el)maxBy
maxBy: <T>(func: (el: T) => number) => (acc: T, el: T) => TUse with: reduce
Returns the largest element by comparing func(element)
[{ a: 1 }, { a: 2 }, { a: 3 }].reduce(maxBy(el => el.a)); // Returns { a: 3 }
const maxBy = <T>(func: (el: T) => number) => (acc: T, el: T) =>
func(el) > func(acc) ? el : accmaxByProperty
maxByProperty: <TObject extends object, TKey extends keyof TObject>(key: TKey) => (acc: TObject, el: TObject) => TObjectUse with: reduce
Returns the largest element by comparing element[key]
[{ a: 1 }, { a: 2 }, { a: 3 }].reduce(maxByProperty("a")); // Returns { a: 3 }
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) => numberUse with: reduce
Returns the smallest value in the list
[1,2,3,4].reduce(min); // Returns 1
const min = (acc: number, el: number) => Math.min(acc, el)minBy
minBy: <T>(func: (el: T) => number) => (acc: T, el: T) => TUse with: reduce
Returns the smallest element by comparing func(element)
[{ a: 1 }, { a: 2 }, { a: 3 }].reduce(minBy(el => el.a)); // Returns { a: 1 }
const minBy = <T>(func: (el: T) => number) => (acc: T, el: T) =>
func(el) < func(acc) ? el : accminByProperty
minByProperty: <TObject extends object, TKey extends keyof TObject>(key: TKey) => (acc: TObject, el: TObject) => TObjectUse with: reduce
Returns the smallest element by comparing element[key]
[{ a: 1 }, { a: 2 }, { a: 3 }].reduce(minByProperty("a")); // Returns { a: 1 }
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: NonNullable<T>) => (x: T) => NonNullable<T>Use with: map
Replaces list elements that are undefined or null with fallback
[1, null, undefined, 2].map(or(0)); // Returns [1, 0, 0, 2]
const or = <T>(fallback: NonNullable<T>) => (x: T): NonNullable<T> =>
isDefined(x) ? x : fallbackpartition
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), []);
// Returns [[{ age: 80 }], [{ age: 10 }]]
const partition = <T>(func: (el: T) => boolean) => (
acc: T[][],
el: T
) => {
const a0 = acc[0] || [],
a1 = acc[1] || [];
if (func(el)) a0.push(el);
else a1.push(el);
return [a0, a1];
}propertyIs
propertyIs: <TObject extends object, TKey extends keyof TObject>(key: TKey, value: TObject[TKey]) => (el: TObject) => booleanUse with: find , filter
Returns true for elements where element[key] equals value
[{ a: 1 }, { a: 2 }].find(propertyIs("a", 2)); // Returns { a: 2 }
[{ a: 1 }, { a: 2 }].filter(propertyIs("a", 2)) // Returns [{ a: 2 }]
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) => booleanUse with: find , filter
Returns true for elements where element[key] exists in list
[{ a: 1 }, { a: 2 }, { a: 3 }].find(propertyIsOneOf("a", [2, 3]));
// ^ Returns { a: 2 }
[{ a: 1 }, { a: 2 }, { a: 3 }].filter(propertyIsOneOf("a", [2, 3]));
// ^ Returns [{ a: 2 }, { a: 3 }]
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) => booleanUse with: find , filter
Returns true for elements where element[key] does not equal value
[{ a: 1 }, { a: 2 }].find(propertyIsnt("a", 2)); // Returns { a: 1 }
[{ a: 1 }, { a: 2 }].filter(propertyIsnt("a", 2)); // Returns [{ a: 1 }]
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) => booleanUse with: find , filter
Returns true for elements where element[key] exists in list
[{ a: 1 }, { a: 2 }, { a: 3 }].find(propertyIsntOneOf("a", [2, 3]));
// ^ Returns { a: 1 }
[{ a: 1 }, { a: 2 }, { a: 3 }].filter(propertyIsntOneOf("a", [2, 3]));
// ^ Returns [{ a: 1 }]
const propertyIsntOneOf = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey,
list: TObject[TKey][]
) => isntOneOfBy(get(key), list)sum
sum: (acc: number, element: number) => numberUse with: reduce
Sum a list of numbers
[1, 2, 3].reduce(sum); // Returns 6
const sum = (acc: number, element: number) => acc + elementsumBy
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); // Returns 3
[1.5, 2.5].reduce(sumBy(Math.floor)); // Returns 3
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) => numberUse 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); // Returns 3
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[]) => booleanUse with: filter
Removes duplicates from list
[1,1,1,2].filter(unique); // Returns [1, 2]
const unique = uniqueBy(el => el)uniqueBy
uniqueBy: <T>(func: (el: T) => unknown) => (el: T, index: number, list: T[]) => booleanUse with: filter
Removes duplicates compared by func(element)
[{ a: 1 }, { a : 1 }].filter(uniqueBy(el => el.a)); // Returns [{ a: 1 }]
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[]) => booleanUse with: filter
Removes duplicates compared by element[key]
[{ a: 1 }, { a: 1 }].filter(uniqueByProperty('a')); // Return [{ a: 1 }]
const uniqueByProperty = <
TObject extends object,
TKey extends keyof TObject
>(
key: TKey
) => uniqueBy<TObject>(get(key))