Froebel - a strictly typed TypeScript utility library.
This is my (WIP) personal collection of TypeScript helper functions and utilities that I use across different projects.
Think an opionated version of lodash, but with first-class types.
function
list
iterable
object
equality
promise
predicate
string
math
data structures
path
Function
ident
(value: T) => T
Identity function.
partial
(fun: T, ...argsLeft: PL) => (...argsRight: PR) => ReturnType<T>
Partially apply a function.
Example
const divide = (dividend: number, divisor: number) => dividend / divisor
// (divisor: number) => number
const oneOver = partial(divide, 1)
// prints: 0.25
console.log(oneOver(4))
forward
(fun: T, ...argsRight: PR) => (...argsLeft: PL) => ReturnType<T>
Given a function and its nth..last arguments, return a function accepting arguments 0..n-1.
Examples
const divide = (dividend: number, divisor: number) => dividend / divisor
// (dividend: number) => number
const divideBy2 = partial(divide, 2)
// prints: 0.5
console.log(divideBy2(1))
const fetchUrl = async (protocol: string, domain: string, path: string) =>
await fetch(`${protocol}://${domain}/${path}`)
const fetchRepo = forward(fetchUrl, 'github.com', 'MathisBullinger/froebel')
const viaHTTPS = await fetchRepo('https')
callAll
(funs: F[], ...args: P) => ReturnTypes<F>
Take a list of functions that accept the same parameters and call them all with the provided arguments.
Example
const mult = (a: number, b: number) => a * b
const div = (a: number, b: number) => a / b
// prints: [8, 2]
console.log( callAll([mult, div], 4, 2) )
bundle
(...funs: λ<T>[]) => (...args: T) => Promise<void>
Given a list of functions that accept the same parameters, returns a function that takes these parameters and invokes all of the given functions.
The returned function returns a promise that resolves once all functions returned/resolved and rejects if any of the functions throws/rejects - but only after all returned promises have been settled.
bundleSync
(...funs: λ<T>[]) => (...args: T) => void
Same as bundle, but return synchronously.
If any of the functions throws an error synchronously, none of the functions after it will be invoked and the error will propagate.
nullishChain
(...funs: [] | [FF, ...FR[]]) => (...args: Parameters<FF>) => ReturnType<FF> | ReturnType<FR[number]>
Given a list of functions that accept the same parameters, returns a function that given these arguments returns the result of the first function whose result is not nullish.
This is equivalent to chaining together invocations of the passed in functions with the given arguments with nullish coalescing (
??
) operators.
Example
const isAdult = (age: number) => { if (n >= 18) return 'adult' }
const isToddler = (age: number) => { if (n <= 3) return 'toddler' }
const ageGroup = nullishChain(isAdult, isToddler, () => 'child')
// this is functionally equivalent to:
const ageGroup = age => isAdult(age) ?? isToddler(age) ?? 'child'
ageGroup(1) // prints: 'toddler'
ageGroup(10) // prints: 'child'
ageGroup(50) // prints: 'adult'
asyncNullishChain
(...funs: [] | [FF, ...FR[]]) => (...args: Parameters<FF>) => Promise<PromType<ReturnType<FF>> | PromType<ReturnType<FR[number]>>>
Same as nullishChain but accept asynchronous functions too.
Example
const readFromCache = (id: string): Resource => { if (id in cache) return cache[id] }
const readFromFile = (id: string): Resource => { if (fileExists(id)) return readFile(id) }
const fetchFromNet = async (id: string): Promise<Resource> => await fetch(`someURL/${id}`)
// async (id: string) => Promise<Resource>
const getResource = asyncNullishChain(readFromCache, readFromFile, fetchFromNet)
throttle
(fun: T, ms: number, opts?: {leading: boolean, trailing: boolean}) => λ<Parameters<T>, void> & {[cancel]: () => void}
Created a throttled function that invokes
fun
at most everyms
milliseconds.
fun
is invoked with the last arguments passed to the throttled function.Calling
[throttle.cancel]()
on the throttled function will cancel the currently scheduled invocation.
debounce
(fun: T, ms: number) => λ<Parameters<T>, void> & {[cancel]: () => void}
Creates a debounced function that delays invoking
fun
untilms
milliseconds have passed since the last invocation of the debounced function.
fun
is invoked with the last arguments passed to the debounced function.Calling
[debounce.cancel]()
on the debounced function will cancel the currently scheduled invocation.
memoize
(fun: T, opt: {limit: number, weak: W, key: (...args: Parameters<T>) => K}) => T & {cache: W extends false ? Map<K, ReturnType<T>> : Cache<K, ReturnType<T>>}
Returns a copy of
fun
that remembers its result for any given arguments and only invokesfun
for unknown arguments.The cache key is computed using the
key
function. The defaultkey
function simply stringifies the arguments.If
limit
is specified, only thelimit
-last entries are kept in cache.The function’s cache is available at
memoized.cache
.If
opt.weak
istrue
, non-primitive cache keys are stored in a WeakMap. This behavior might for example be useful if you want to memoize some calculation including a DOM Node without holding on to a reference of that node. Using weak keys prohibits setting alimit
.
Examples
const expensiveCalculation = (a: number, b: number) => {
console.log(`calculate ${a} + ${b}`)
return a + b
}
const calc = memoize(expensiveCalculation)
console.log( calc(1, 2) )
// calculate 1 + 2
// 3
console.log( calc(20, 5) )
// calculate 20 + 5
// 25
console.log( calc(20, 5) )
// 25
console.log( calc(1, 2) )
// 3
calc.cache.clear()
console.log( calc(1, 2) )
// calculate 1 + 2
// 3
const logIfDifferent = memoize(
(msg: string) => console.log(msg),
{
limit: 1,
key: msg => msg
}
)
logIfDifferent('a')
logIfDifferent('a')
logIfDifferent('b')
logIfDifferent('a')
// a
// b
// a
limitInvocations
(fun: T, limit: number, ...funs: ExcS<T>) => T
Returns a version of the function
fun
that can only be invokedlimit
times. An optionalexcept
function will be called with the same parameters on any additional invocations.If
fun
returns anything butvoid
(orPromise<void>
), supplying anexcept
function is mandatory.The
except
function must have the same return type asfun
, or — iffun
returns a promise — it may return the type that the promise resolves to synchronously.The
except
function may also throw instead of returning a value.
once
(fun: T, ...funs: ExcS<T>) => T
Special case of limitInvocations.
fun
can only be invoked once.see limitInvocations
List
atWrap
(arr: T[], i: number) => T
Access list at
i % length
. Negative indexes start indexing the last element as[-1]
and wrap around to the back.
zip
(...lists: T) => Zip<T>
Takes multiple lists and returns a list of tuples containing the value in each list at the current index. If the lists are of different lengths, the returned list of tuples has the length of the shortest passed in list.
Example
const pairs = zip([1,2,3], ['a','b','c'])
console.log(pairs) // prints: [[1,'a'], [2,'b'], [3,'c']]
zipWith
(zipper: (...args: {[I in string | number | symbol]: U}) => U, ...lists: T) => U[]
Same as zip but also takes a
zipper
function, that is called for each index with the element at current index in each list as arguments. The result ofzipper
is the element at current index in the list returned fromzipWith
.
Example
const sums = zipWith((a,b) => a+b, [1,2,3], [4,5,6])
console.log(sums) // prints: [5,7,9]
unzip
(...zipped: T[][]) => Unzip<T>
Reverse of zip. Takes a list of tuples and deconstructs them into an array (of length of the tuples length) of lists each containing all the elements in all tuples at the lists index.
Example
const [nums, chars] = unzip([1,'a'], [2,'b'], [3,'c'])
console.log(nums) // prints: [1, 2, 3]
console.log(chars) // prints: ['a','b','c']
unzipWith
(zipped: T[][], ...unzippers: U) => {[I in string | number | symbol]: ReturnType<U[I]>}
Same as unzip but accepts an
unzipper
function for each tuple index. Theunzipper
’s return value is used as the value in the list at that index returned fromunzipWith
.The
unzipper
takes the current element as its first argument, an accumulator as second argument (initiallyundefined
) and its return value is the accumulator passed into the next invocation.
Example
const [nums, str] = unzip(
[ [1,'a'], [2,'b'], [3,'c'] ],
(n, acc: number[] = []) => [...acc, n],
(c, str = '') => str + c
)
console.log(nums) // prints: [1, 2, 3]
console.log(str) // prints: 'abc'
batch
(list: T[], batchSize: number) => T[][]
Takes a
list
and returns it in multiple smaller lists of the sizebatchSize
. The last batch may be smaller thanbatchSize
depending on iflist
size is divisible bybatchSize
.
Example
batch([1,2,3,4,5], 2) // -> [ [1,2], [3,4], [5] ]
partition
(list: T[], predicate: (el: T) => el is S) => [S[], Exclude<T, S>[]]
Takes a
list
and returns a pair of lists containing: the elements that match thepredicate
and those that don’t, respectively.Think of it as
filter
, but the elements that don’t pass the filter aren’t discarded but returned in a separate list instead.
Example
const [strings, numbers] = partition(
['a', 'b', 1, 'c', 2, 3],
(el): el is string => typeof el === 'string'
)
// strings: ["a", "b", "c"]
// numbers: [1, 2, 3]
take
(n: number, list: Iterable<T>) => T[]
Takes
n
elements from the iterablelist
and returns them as an array.
Example
take(5, repeat(1, 2)) // -> [1, 2, 1, 2, 1]
take(3, [1, 2, 3, 4]) // -> [1, 2, 3]
take(3, [1, 2]) // -> [1, 2]
range
Creates a range between two values.
see numberRange and alphaRange
numberRange
(start: number, end: number, step: number) => number[]
Constructs a numeric between
start
andend
inclusively.
Example
range(2, 6) // -> [2, 3, 4, 5, 6]
range(8, 9, .3) // -> [8, 8.3, 8.6, 8.9]
range(3, -2) // -> [3, 2, 1, 0, -1, -2]
alphaRange
(start: string, end: string) => string[]
Constructs a range between characters.
Example
range('a', 'd') // -> ['a', 'b', 'c', 'd']
range('Z', 'W') // -> ['Z', 'Y', 'X', 'W']
Iterable
repeat
(...sequence: [T, ...T[]]) => Generator<T>
Returns a generator that repeats
sequence
.
Example
// prints: 1, 2, 3, 1, 2, 3, ...
for (const n of repeat(1, 2, 3))
console.log(n)
take
(n: number, list: Iterable<T>) => Generator<T>
Takes
n
elements from the iterablelist
and returns them as a generator.
Example
[...take(5, repeat(1, 2))] // -> [1, 2, 1, 2, 1]
[...take(3, [1, 2, 3, 4])] // -> [1, 2, 3]
[...take(3, [1, 2])] // -> [1, 2]
Object
pick
(obj: T, ...keys: K[]) => Pick<T, K>
From
obj
, create a new object that only includeskeys
.
Example
pick({ a: 1, b: 2, c: 3 }, 'a', 'c') // { a: 1, c: 3 }
omit
(obj: T, ...keys: K[]) => Omit<T, K>
From
obj
, create a new object that does not includekeys
.
Example
omit({ a: 1, b: 2, c: 3 }, 'a', 'c') // { b: 2 }
Equality
oneOf
(value: T, ...cmps: TT) => value is TT[number]
Checks if
v
is one ofcmps
.
equal
(a: unknown, b: unknown) => boolean
Checks if
a
andb
are structurally equal using the following algorithm:
- primitives are compared by value
- functions are compared by reference
- objects (including arrays) are checked to have the same properties and their values are compared recursively using the same algorithm
clone
(value: T) => T
Returns a copied version of
value
.If
value
is primitive, returnsvalue
. Otherwise, properties ofvalue
are copied recursively. Onlyvalue
’s own enumerable properties are cloned. Arrays are cloned by mapping over their elements.If a path in
value
references itself or a parent path, then in the resulting object that path will also reference the path it referenced in the original object (but now in the resuling object instead of the original).
Promise
isPromise
(value: unknown) => value is Promise<T>
Checks if
value
looks like a promise.
isNotPromise
(value: T) => value is Exclude<T, Promise<any>>
Checks if
value
is not a promise.
Example
(value: number | Promise<unknown>) => {
if (isNotPromise(value)) return value / 2
}
Predicate
truthy
(value: T) => value is PickTruthy<T>
Checks if
value
is truthy. Literal types are narrowed accordingly.
falsy
(value: T) => value is PickFalsy<T>
Checks if
value
is falsy. Literal types are narrowed accordingly.
nullish
(value: T) => value is Nullish<T>
Checks if
value
is nullish. Literal types are narrowed accordingly.
notNullish
(value: null | T) => value is T
Checks if
value
is not nullish. Literal types are narrowed accordingly.
Example
const nums = (...values: (number | undefined)[]): number[] => values.filter(notNullish)
isFulfilled
(result: PromiseSettledResult<T>) => result is PromiseFulfilledResult<T>
Checks if
result
(returned fromPromise.allSettled
) is fulfilled.
isRejected
(result: PromiseSettledResult<unknown>) => result is PromiseRejectedResult
Checks if
result
(returned fromPromise.allSettled
) is rejected.
String
prefix
(prefix: T0, str: T1, caseMod?: C) => `${string}`
Returns
str
prefixed withprefix
. Optionally, allows prefxing in camel case, i.e.prefix('foo', 'bar', 'camel') => 'fooBar'
, or snake case, i.e.prefix('foo', 'bar', 'snake') => 'foo_bar'
.The result is strictly typed, so
prefix('foo', 'bar')
will return the type'foobar'
, not just a genericstring
.
suffix
(str: T1, suffix: T0, caseMod?: C) => `${string}`
Returns
str
suffixed withsuffix
. Same case and type behavior as prefix.
capitalize
(str: T) => Capitalize
Upper-case first letter of string.
uncapitalize
(str: T) => Uncapitalize
Lower-case first letter of string
upper
(str: T) => Uppercase
Strictly typed
String.toUpperCase()
.
lower
(str: T) => Lowercase
Strictly typed
String.toLowerCase()
.
snake
(str: T) => SnakeCase<T>
Transforms a variable name to snake case.
Note: The rules for transforming anything to snake case are somewhat vague. So use this only for very simple names where the resulting value is absolutely unambiguous. For more examples of how names are transformed, have a look at the test cases.
Example
snake('fooBar') // 'foo_bar'
camel
(str: T) => CamelCase<T>
Transforms a variable name to camel case.
Note: The rules for transforming anything to camel case are somewhat vague. So use this only for very simple names where the resulting value is absolutely unambiguous. For more examples of how names are transformed, have a look at the test cases.
Example
camel('foo_bar') // 'fooBar'
transformCase
(str: T, targetCase: C) => SnakeCase<T>
Transform a variable name to
targetCase
Math
clamp
(min: number, num: number, max: number) => number
Clamp
num
betweenmin
andmax
inclusively.
Data Structures
BiMap
class BiMap<L, R>(data?: Map<L, R> | [L, R][], aliasLeft?: AL, aliasRight?: AR)
Bidirectional map. Maps two sets of keys in a one-to-one relation.
Both sides are accessible (at .left & .right, or at their respective alias if one was provided in the constructor) with an interface similar to that of the built-in Map and the same iteration behavior.
Examples
const nums = BiMap.from({ one: 1, two: 2 })
// different ways of iterating over the entries
[...nums.left] // [['one',1], ['two',2]]
[...nums.right] // [[1,'one'], [2,'two']]
[...nums.left.keys()] // ['one', 'two']
[...nums.left.values()] // [1, 2]
[...nums.right.keys()] // [1, 2]
[...nums.right.values()] // ['one', 'two']
[...nums] // [['one',1], ['two',2]]
[...nums.right.entries()] // [[1,'one'], [2,'two']]
Object.fromEntries(nums.right) // { '1': 'one', '2': 'two' }
// setting a value
nums.left.three = 3
// when accessing a property using bracket notation (i.e. nums.right[4]),
// JavaScript coerces the key to a string, so keys that aren't strings or
// symbols must be accessed using the same access methods known from Map.
nums.right.set(4, 'four')
// remapping values
nums.left.tres = 3 // {one: 1, two: 2, tres: 3, four: 4}
nums.right.set(4, 'cuatro') // {one: 1, two: 2, tres: 3, cuatro: 4}
// deleting
delete nums.left.tres // {one: 1, two: 2, cuatro: 4}
nums.right.delete(4) // {one: 1, two: 2}
// reversing the map
const num2Name = nums.reverse()
console.log([...num2Name.left]) // [[1,'one'], [2,'two']]
console.log(Object.fromEntries(num2Name.right)) // {one: 1, two: 2}
// other methods known from built-in Map
nums.size // 2
nums.[left|right].size // 2
nums.clear() // equivalent to nums.[left|right].clear()
console.log(nums.size) // 0
// giving aliases to both sides
const dictionary = new BiMap(
[
['hello', 'hallo'],
['bye', 'tschüss'],
],
'en',
'de'
)
dictionary.de.get('hallo') // 'hello'
dictionary.en.get('bye') // 'tschüss'
delete dictionary.de.hallo
console.log(Object.fromEntries(dictionary.en)) // { bye: 'tschüss' }
// you can also use the BiMap.alias method:
BiMap.alias('en', 'de')<string, string>()
BiMap.alias('en', 'de')([['hello', 'hallo']])
BiMap.alias('en', 'de')(new Map<string, string>())
BiMap.alias('en', 'de')({ hello: 'hallo' })
BiMap.alias('en', 'de')(new Set(['hello']), new Set(['hallo']))
// the same arguments can be used with BiMap.from, e.g.:
BiMap.from(new Set<number>(), new Set<number>())
SortedArray
class SortedArray<T>(compare: Cmp<T>, ...value: T[])
Sorted array. Behaves much like a regular array but its elements remain sorted using the
compare
function supplied in the constructor.Contains most of the methods defined on regular JavaScript arrays as long as they don’t modify the array’s content in place.
New elements are added using the
add(...values)
method.Elements can still be accessed using bracket notation as in plain JavaScript arrays but can’t be assigned to using bracket notation (as that could change the element’s sort position).
Elements can be removed using the
delete(...indices)
method, which returns an array containing the deleted values. Deleting an element usingdelete sorted[index]
will also work, but results in a TypeScript error because element access is marked readonly.Array methods that pass a reference of the array to a callback (e.g.
map
,reduce
,find
) will pass a reference to the SortedArray instance instead.The
filter
andslice
methods will return SortedArray instances instead of plain arrays.
SortedMap
class SortedMap<K, V>(compare: Cmp<K, V>, entries?: null | [K, V][])
Behaves like a regular JavaScript
Map
, but its iteration order is dependant on thecompare
function supplied in the constructor.Note: The item’s sort position is only computed automatically on insertion. If you update one of the values that the
compare
function depends on, you must call theupdate(key)
method afterwards to ensure the map stays sorted.
Path
select
(obj: T, ...path: P) => PickPath<T, P>
Returns the value in
obj
atpath
. If the given path does not exist, the symbolnone
is returned.