Skip to content

Instantly share code, notes, and snippets.

@ppeelman
Last active April 12, 2023 09:45
Show Gist options
  • Save ppeelman/68eea527a1dc8cac81e3edcb10eb4641 to your computer and use it in GitHub Desktop.
Save ppeelman/68eea527a1dc8cac81e3edcb10eb4641 to your computer and use it in GitHub Desktop.
Functional programming in TypeScript
// If you are new to functional programming in JavaScript, the following is a must read!
// https://github.com/MostlyAdequate/mostly-adequate-guide
// Pipe and compose
// =================
// https://dev.to/ascorbic/creating-a-typed-compose-function-in-typescript-3-351i
export const pipe = <T extends any[], R>(fn1: (...args: T) => R, ...fns: Array<(a: R) => R>) => {
const piped = fns.reduce(
(prevFn, nextFn) => (value: R) => nextFn(prevFn(value)),
(value) => value
);
return (...args: T) => piped(fn1(...args));
};
export const compose = <R>(fn1: (a: R) => R, ...fns: Array<(a: R) => R>) =>
fns.reduce((prevFn, nextFn) => (value) => prevFn(nextFn(value)), fn1);
// Generic functions
// ==================
// Stil work to do on the types! As much generic types as possible (joepie!)
export const firstIndex = <T>(array: T[]): T | undefined => array?.[0];
export const isNull = (val: any): val is null => val === null;
export const isUndefined = (val: any): val is undefined => val === undefined;
export const isNil = (val: any) => isNull(val) || isUndefined(val);
export const notNil = (val: any) => !isNil(val);
export const log = (logger: NGXLogger, label: string) => (val: any) => logger.info(label, val);
export const not = (bool: boolean): boolean => !bool;
export const capitalize = ([firstLetter, ...restOfWord]: string): string => firstLetter.toUpperCase() + restOfWord.join('');
export const largerThanOrEqual =
(num2: number, ...args: any[] | any) =>
(num1: number) =>
num1 >= num2;
export const filterArray =
<T extends any = any>(fn: (item: T) => boolean) =>
(arr: T[]): T[] =>
arr.filter(fn);
export const mapArray =
<T extends any = any, R extends any = any>(fn: (item: T) => R) =>
(arr: T[]): R[] =>
arr.map(fn);
export const propEquals =
<T extends object = any, K extends keyof T = any>(prop: K, equalValue: T[K]) =>
(obj: T) =>
obj?.[prop] === equalValue;
export const pick =
<T extends object = any, K extends keyof T = any>(key: K) =>
(obj: T): T[K] | undefined =>
obj?.[key];
export const numofDigits = (num: number): number => num.toString().length;
// More info about keyof/lookup types: https://mariusschulz.com/blog/keyof-and-lookup-types-in-typescript
export const getDistinctObjectsByProp = <T extends object, K extends keyof T>(arr: T[], propName: K) => {
return Array.from(new Set(arr.map((item: T) => item?.[propName]))).map((uniqueVal: T[K]) =>
arr.find((i: T) => uniqueVal === i[propName])
);
};
export const subset = <T extends object, K extends keyof T>(keys: K[], obj: T): Pick<T, K> => {
return keys
.filter(key => key in obj)
.reduce((obj2: Pick<T, K>, key: keyof T) => ({...obj2, [key]: obj[key]}), {} as Pick<T, K>);
}
export const last = <T>(arr: T[]): T => arr[arr.length - 1];
export const removeProperty = <T extends object, K extends keyof T>(propName: K, obj: T): Omit<T, K> => {
const copy = {...obj};
delete copy[propName];
return copy;
}
// A curried, IMMUTABLE sort to sort an array of objects by one of the properties on the object
// works also for properties on numbers, strings, dates (eg. length for a string)
// The function is typed to accept only properties which are those types.
// Defaults to ascending sort (lower values first)
// Inspired by: https://javascript.plainenglish.io/react-and-typescript-generic-search-sort-and-filter-879c5c3e2f0e
export const sortBy =
<T>(property: Extract<keyof T, string | number | Date>, isAscending: boolean = true) =>
(arr: T[]) => {
const copy = [...arr];
copy.sort((objectA: T, objectB: T): number => {
const result = () => {
if (objectA[property] > objectB[property]) {
return 1;
} else if (objectA[property] < objectB[property]) {
return -1;
} else {
return 0;
}
};
return isAscending ? result() : result() * -1;
});
return copy;
};
/**
* Takes a predicate and a list of values and returns a a tuple (2-item array),
* with each item containing the subset of the list that matches the predicate
* and the complement of the predicate respectively
*
* @sig (T -> Boolean, T[]) -> [T[], T[]]
*
* @param {Function} predicate A predicate to determine which side the element belongs to.
* @param {Array} arr The list to partition
*
* Inspired by the Ramda function of the same name
* @see https://ramdajs.com/docs/#partition
*
* @example
*
* const isNegative: (n: number) => boolean = n => n < 0
* const numbers = [1, 2, -4, -7, 4, 22]
* partition(isNegative, numbers)
* // => [ [-4, -7], [1, 2, 4, 22] ]
*/
export const partition = <T>(
predicate: (val: T) => boolean,
arr: Array<T>,
): [Array<T>, Array<T>] => {
const partitioned: [Array<T>, Array<T>] = [[], []]
arr.forEach((val: T) => {
const partitionIndex: 0 | 1 = predicate(val) ? 0 : 1
partitioned[partitionIndex].push(val)
})
return partitioned
}
export const removeProperty = <T extends object, K extends keyof T>(propName: K, obj: T): Omit<T, K> => {
const copy = {...obj};
delete copy[propName];
return copy;
}
export const groupBy = <T, K extends string>(items: T[], selector: (item: T) => K): {[key: string]: T[]} => {
return items.reduce((group: {[key: string]: T[]}, item: T) => {
const keyVal = selector(item);
group[keyVal] = group[keyVal] ?? [];
group[keyVal].push(item);
return group;
}, {} as {[key: string]: T[]});
}
export function debounce<F extends Function>(func: F, wait: number): F {
let timeoutID: number;
if (!Number.isInteger(wait)) {
console.warn('Called debounce without a valid number')
wait = 300;
}
// conversion through any necessary as it won't satisfy criteria otherwise
return <any>function (this: any, ...args: any[]) {
clearTimeout(timeoutID);
const context = this;
timeoutID = window.setTimeout(function () {
func.apply(context, args);
}, wait);
};
}
export const getUniqueBySelector = <T extends object, K = any>(arr: T[], selector: (item: T) => K): T[] => {
if(!Array.isArray(arr)) {
throw Error('The first argument should be an array!')
}
if(arr.length === 0) {
return arr;
}
const uniqueValues = new Map();
return arr.filter((item: T) => {
if (uniqueValues.get(selector(item))) {
return false;
}
uniqueValues.set(selector(item), true);
return true;
});
}
export const getUniqueByProp = <T extends object, K extends keyof T>(arr: T[], prop: K): T[] => {
if(!Array.isArray(arr)) {
throw Error('The first argument should be an array!')
}
if(arr.length === 0) {
return arr;
}
const element = arr[0];
if(typeof element !== 'object') {
throw Error(`The first argument should be an array of objects. However, the first element of the array is of type ${typeof element}`)
}
if(!(prop in element)) {
throw Error(`The first element of the array does not include property '${prop}'`)
}
const uniqueValues = new Map();
return arr.filter((item: T) => {
if (uniqueValues.get(item[prop])) {
return false;
}
uniqueValues.set(item[prop], true);
return true;
});
export interface ItemCount<T> {
count: number;
items: T[];
}
export const countAndGroupItems = <T extends any>(items: T[], selector: (item: T) => string): ItemCount<T>[] => {
interface IntermediateResult {
[key: string ]: ItemCount<T>
}
const intermediateResult = items.reduce((result: IntermediateResult, item: T) => {
const keyVal: string = selector(item);
if(result[keyVal]) {
result[keyVal].count++;
result[keyVal].items.push(item);
} else {
result[keyVal] = {items: [item], count: 1}
}
return result;
}, {} as IntermediateResult)
return Object.values(intermediateResult);
}
export function isEqual(obj1, obj2) {
if (obj1 === obj2) return true;
if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 == null || obj2 == null) {
return false;
}
const keysA = Object.keys(obj1);
const keysB = Object.keys(obj2);
if (keysA.length !== keysB.length) {
return false;
}
let result = true;
keysA.forEach((key) => {
if (!keysB.includes(key)) {
result = false;
}
if (typeof obj1[key] === 'function' || typeof obj2[key] === 'function') {
if (obj1[key].toString() !== obj2[key].toString()) {
result = false;
}
}
if (!isEqual(obj1[key], obj2[key])) {
result = false;
}
});
return result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment