Last active
April 12, 2023 09:45
-
-
Save ppeelman/68eea527a1dc8cac81e3edcb10eb4641 to your computer and use it in GitHub Desktop.
Functional programming in TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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