Antiutils
TypeScript/JavaScript utilities for those who don't like utilities.
Installing
yarn add antiutils
or
npm install antiutils --save
Minimal API
This library provides a utility only when something can't be easily and readably accomplished with vanilla JavaScript. There are three reasons: first, the code is easier to write and refactor when there is "only one way to do it"; second, we'd all much rather use a universally understood language than a dialect; and third, a large utility library shapes your whole codebase, and we the authors do not want to be telling you how to write your code (let TC39 do it).
pipe
function
The library provides a function pipe
which takes between 1 and 12 arguments. pipe(x, a, b)
is equivalent to b(a(x))
, in other words, this function pipes a value through a number of functions in the order that they appear. This article talks about why this function is useful.
💡 TIPAt any point in the pipeline, you can insert the
log
function from 1log library to log piped values.
Non-mutating functions for working with objects, arrays, maps, and sets
Objects:
💡 TIPIf you use TypeScript 4.1+, you can enable strictly checked indexed access using
--noUncheckedIndexedAccess
compiler flag.
Arrays:
Maps:
Sets:
Functions for working with native iterables
How-to:
-
Filter an iterable in a way that the type system understands:
pipe( [1, undefined], // Equivalent to `filterIterable((value) => value !== undefined)`. flatMapIterable((value) => (value !== undefined ? [value] : [])), );
(type will be inferred as
IterableIterator<number>
, notIterableIterator<number | undefined>
as would be the case if you usedfilterIterable
; the same trick works when filtering arrays and observables). -
Index elements:
zipIterables(rangeIterable(), yourIterable)
(returns an iterable of[<element index>, <element>]
). -
Get a flag indicating if the element is the first element:
zipIterables(firstIterable(), yourIterable)
(returns an iterable of[boolean, <element>]
). -
Count elements in an iterable:
pipe(yourIterable, reduceIterable(countReducer, 0))
. -
Find the first element matching a predicate:
pipe(yourIterable, filter(yourPredicate), firstInIterable)
. -
Yield values while a condition holds:
pipe( [1, 2, 3, 2], scanIterable((_, value) => (value <= 2 ? value : undefined)), );
(yields
1
,2
, see Reducers andscanIterable
).
Comparison functions
The library exports types
interface CompareFunction<T> {
(to: T, from: T): number;
}
interface EqualFunction<T> {
(from: T, to: T): boolean;
}
It provides implementations of CompareFunction
for primitive types:
and a function lexicographicCompare
to compose CompareFunction
s.
It also provides implementations of EqualFunction
for objects, iterables, maps, and sets:
and a function deepEqual
that recursively delegates to those functions depending on the object type.
Reducers
The library exports types
interface Reducer<Accumulator, Element> {
(accumulator: Accumulator, element: Element): Accumulator;
}
interface PartialReducer<Accumulator, Element> {
(accumulator: Accumulator, element: Element): Accumulator | undefined;
}
Reducer
is a regular reducer that can be passed to reduce
method of an array. PartialReducer
is like a regular reducer, but can return undefined
to indicate that the current value of the accumulator should be used as the final result, so functions reduceIterable
and scanIterable
will stop the iteration short.
The library provides the following implementations of Reducer
:
and the following implementations of PartialReducer
:
Lenses
In Antiutils the definition of a lens is based on the concept of a view, which is a combination of a getter and a setter:
interface View<S, A> {
get: () => A;
set: (value: A) => S;
}
When generic type S
is void
, the setter only performs a side effect - we call this type of view a state view:
type StateView<A> = View<void, A>;
State views are useful when working with React components - for details, see package antiutils-react
which provides glue between Antiutils and React.
The setter can also be a pure function that performs a non-mutating update, as in the following view that lets you access property a
in object { a: 1, b: 2 }
:
const view: View<{ a: number }, number> = {
get: () => 1,
set: (value) => ({ a: value, b: 2 }),
};
A lens is defined as a function that transforms one view into another view:
interface Lens<S, A, B> {
(source: View<S, A>): View<S, B>;
}
The library provides the following utilities:
-
objectProp
: returns a lens that zooms in on an object's property. -
mapProp
: returns a lens that zooms in on a value stored in aMap
under a specific key. -
setProp
: returns a lens that zooms in on presence of an element in aSet
. -
rootView
: a function that converts avalue
into a view{ get: () => value, set: <identity function> }
.
Here is an example using objectProp
and rootView
:
type State = { a: { b: string; c: string } };
/**
* A reducer that sets the value of `b` in the state to the value provided
* as action payload.
**/
const sampleReducer = (state: State, action: { payload: string }) =>
pipe(
// Returns `View<State, State>`.
rootView(state),
// Transforms values into `View<State, { b: string; c: string }>`.
objectProp('a'),
// Transforms values into `View<State, string>`.
objectProp('b'),
)
// `set` takes a value for `b` and returns a new `State`.
.set(action.payload);
expect(sampleReducer({ a: { b: '', c: '' } }, { payload: 'x' })).toEqual({
a: { b: 'x', c: '' },
});
In the code above, TypeScript successfully infers the types, and as we get to a point where we need to type 'a', 'b', or 'c', IntelliSense shows correct suggestions.
A similar example, but with property a
optional:
type State = { a?: { b: string; c: string } };
const sampleReducer = (state: State, action: { payload: string }) =>
pipe(
rootView(state),
// Transforms values into `View<State, { b: string; c: string } |
// undefined>`.
objectProp('a'),
// Transforms values into `View<State, { b: string; c: string }>`.
({ get, set }) => ({
get: () => get() ?? { b: '', c: '' },
set,
}),
objectProp('b'),
).set(action.payload);
expect(sampleReducer({}, { payload: 'x' })).toEqual({
a: { b: 'x', c: '' },
});
💡 TIPYou can log views using a plugin from package 1log-antiutils.
Memoization
The library provides utilities memoizeWeak
and memoizeStrong
to memoize functions that take a single argument. Internally they cache results in respectively a WeakMap and a Map, with arguments as keys and results as values. memoizeWeak
has an advantage that retaining a reference to the memoized function will not prevent cached arguments and results from being garbage-collected, but it can only memoize functions that take objects (not primitive values) as arguments, because only objects can be used as keys in a WeakMap.
💡 TIPYou can combine
memoizeWeak
andmemoizeStrong
to memoize a function that takes multiple arguments, some of them primitive values, e.g.const original = (x: { a: number }, y: number) => x.a + y; const memoized = memoizeWeak((x: { a: number }) => memoizeStrong((y: number) => original(x, y)), ); const withRestoredSignature = (x: { a: number }, y: number) => memoized(x)(y);
The library also provides a function teach
for cases when you need to teach a memoized function to return a result already known from an external source such as persisted storage, and a function knows
to check if there is a cached result for a given argument.
Functions for downcasting
The library provides the following identity functions that cast the argument to a type, but unlike TypeScript's as
, never make type assertions:
-
asNever
: a function which has signature(value: never) => never
and which throws if called, used to typecheck that a symbol has typenever
and therefore the call site is unreachable. For example, ifa
has type0 | 1
, you could writea === 0 ? 'zero' : a === 1 ? 'one' : asNever(a)
to make sure that all possibilities fora
have been exhausted. If the type ofa
changes to say0 | 1 | 2
, the type of the argument passed toasNever
will be inferred as2
, and this will cause a typechecking error because the only type assignable tonever
isnever
itself. -
as
: an identity function with signature<T>(value: T) => T
that can be used to downcast a value to a non-generic type:as<YourType>(yourValue)
. -
asContext
: an identity function with signature<A, B extends A>(value: B) => A
that can be used to infer the type of a value from the context instead of the other way around. As an example, consider the following code that throws if the iterable has more than 1 element:pipe( <some iterable>, reduceIterable( asContext(() => { throw <some error>; }), ), );
Without wrapping the reducer in
asContext
, TypeScript would infer the type of the accumulator asnever
and the code would not typecheck. -
asCompareFunction
,asEqualFunction
,asLens
,asReducer
,asPartialReducer
,asStateView
,asView
: identity functions that can be used to downcast values to any of the generic types defined by the library.