Skip to content

@absoluteui/helpers/fn

Debounce, throttle, memoize, once, pipe, and compose.

import { ... } from '@absoluteui/helpers/fn';

Small wrappers that change when or how often a function runs: debounce and throttle for input and scroll events, memoize for repeated lookups, once for setup, safeCall for callbacks you do not control, and pipe/compose for building transformations. For promise work (retries, timeouts, concurrency), use @absoluteui/helpers/async. The Helpers and services guide covers import and error conventions.

How do I search as the user types without a request per keystroke?

Section titled “How do I search as the user types without a request per keystroke?”

debounce waits until typing pauses. Cancel the pending call when the view closes, so a late search does not update a removed list.

import { debounce } from '@absoluteui/helpers/fn';
const requests: string[] = [];
const search = debounce((term: string) => { requests.push(term.trim()); }, 250);
for (const typed of ['r', 'ra', 'rao', 'rao ']) search(typed);
search.pending(); // => true
search.flush();
requests; // => ['rao'] // one request, with the last value
search('liam');
search.cancel(); // e.g. in disconnectedCallback
search.pending(); // => false
requests.length; // => 1

How do I autosave a long form without losing the last edit?

Section titled “How do I autosave a long form without losing the last edit?”

Add maxWait so a user who never pauses still gets saved regularly, and call flush when the page is hidden so the final change is written.

import { debounce } from '@absoluteui/helpers/fn';
declare function saveDraft(draft: { notes: string }): void;
const autosave = debounce(saveDraft, 1000, { maxWait: 10_000 });
document.querySelector('textarea')?.addEventListener('input', (e) => {
if (e.target instanceof HTMLTextAreaElement) autosave({ notes: e.target.value });
});
addEventListener('pagehide', () => autosave.flush());

How do I cache lookups, including async ones?

Section titled “How do I cache lookups, including async ones?”

memoize keeps a bounded LRU cache (1000 entries by default) with an optional TTL; give it a key when arguments are objects or several values. Returned promises are cached too, including ones that reject, so delete the entry on failure to allow a retry.

import { memoize } from '@absoluteui/helpers/fn';
interface Manager { employeeId: string; asOf: string; manager: string }
let calls = 0;
const fetchManager = async (employeeId: string, asOf: string): Promise<Manager> => {
calls++;
if (employeeId === 'E-404') throw new Error('Not found');
return { employeeId, asOf, manager: 'M-204' };
};
const managerOf = memoize(
(id: string, asOf: string): Promise<Manager> => fetchManager(id, asOf).catch((e: unknown) => { managerOf.delete(id, asOf); throw e; }),
{ key: (id, asOf) => `${id}@${asOf}`, ttlMs: 5 * 60_000, maxSize: 500 },
);
(await managerOf('E-1001', '2026-10-01')).manager; // => 'M-204'
await managerOf('E-1001', '2026-10-01');
calls; // => 1
await managerOf('E-404', '2026-10-01').catch(() => 'failed'); // => 'failed'
managerOf.size; // => 1 // the failure was not kept

How do I recompute a filtered list only when its inputs change?

Section titled “How do I recompute a filtered list only when its inputs change?”

memoizeOne remembers just the latest call, which suits render code: the same rows and filter return the same array, so identity checks downstream stay cheap.

import { memoizeOne } from '@absoluteui/helpers/fn';
interface Employee { name: string; dept: string }
const byDept = memoizeOne((rows: readonly Employee[], dept: string) => rows.filter((r) => r.dept === dept));
const rows: Employee[] = [{ name: 'Asha Rao', dept: 'hr' }, { name: 'Liam Chen', dept: 'finance' }];
const first = byDept(rows, 'hr');
byDept(rows, 'hr') === first; // => true
byDept(rows, 'finance') === first; // => false
byDept([...rows], 'hr') === first; // => false // a new array is a new input

How do I run setup once and keep a user’s callback from breaking mine?

Section titled “How do I run setup once and keep a user’s callback from breaking mine?”

once returns the first result on every later call (and rethrows the first error). safeCall runs an optional callback, reports its error, and lets your code continue.

import { once, safeCall } from '@absoluteui/helpers/fn';
let loads = 0;
const loadHolidays = once(() => { loads++; return ['2026-12-25', '2027-01-01']; });
loadHolidays();
loadHolidays().length; // => 2
loads; // => 1
const logged: string[] = [];
const options: { onApprove?: (id: string) => void } = { onApprove: () => { throw new Error('Handler failed'); } };
safeCall(() => options.onApprove?.('LR-77'), (e) => logged.push(e.message)); // => undefined
logged; // => ['Handler failed']
  • debounce and throttle do not forward this: wrap methods in an arrow function (debounce(() => this.save(), 300)).
  • throttle runs the first call immediately and merges the rest into one trailing call; pass { leading: false } to wait first.
  • memoize keys by the first argument unless you pass key, so calls that differ only in later arguments share an entry.
import { memoize } from '@absoluteui/helpers/fn';
const label = memoize((id: string, lang: string) => `${id}:${lang}`);
label('E-1', 'en');
label('E-1', 'de'); // => 'E-1:en' // add { key: (id, lang) => `${id}|${lang}` }

after

Calls fn only from the nth call onwards; earlier calls return undefined.

function after<A extends unknown[], R>(n: number, fn: (...args: A) => R): (...args: A) => R | undefined;

Example:

const uploads = ['contract.pdf', 'id-card.png'];
const done = after(uploads.length, () => 'All files uploaded.');
[done(), done()]; // => [undefined, 'All files uploaded.']

attempt

Runs fn, returning a Result instead of throwing.

function attempt<R>(fn: () => R): Result<R>;

Example:

const r = attempt(() => JSON.parse('{"active":true}') as unknown); // => { ok: true, value: { active: true } }
attempt(() => JSON.parse('{')).ok; // => false

before

Calls fn for the first n - 1 calls; afterwards returns the last result without calling it.

function before<A extends unknown[], R>(n: number, fn: (...args: A) => R): (...args: A) => R | undefined;

Example:

let attempts = 0;
const tryLogin = before(3, () => ++attempts); // runs on calls 1 and 2 only
[tryLogin(), tryLogin(), tryLogin()]; // => [1, 2, 2]

callIf

Calls fn(...args) when condition is truthy; otherwise returns undefined.

function callIf<A extends unknown[], R>(condition: unknown, fn: (...args: A) => R, ...args: A): R | undefined;

Example:

const add = (a: number, b: number): number => a + b;
callIf(true, add, 2, 3); // => 5
callIf(false, add, 2, 3); // => undefined

compose

Right-to-left composition: compose(g, f)(x) === g(f(x)). The last function may take several arguments.

function compose<A extends unknown[], B>(ab: (...a: A) => B): (...a: A) => B;
function compose<A extends unknown[], B, C>(bc: (b: B) => C, ab: (...a: A) => B): (...a: A) => C;
function compose<A extends unknown[], B, C, D>(cd: (c: C) => D, bc: (b: B) => C, ab: (...a: A) => B): (...a: A) => D;
function compose<A extends unknown[], B, C, D, E>(de: (d: D) => E, cd: (c: C) => D, bc: (b: B) => C, ab: (...a: A) => B): (...a: A) => E;
function compose<A extends unknown[], B, C, D, E, F>(ef: (e: E) => F, de: (d: D) => E, cd: (c: C) => D, bc: (b: B) => C, ab: (...a: A) => B): (...a: A) => F;
function compose<A extends unknown[], B, C, D, E, F, G>(fg: (f: F) => G, ef: (e: E) => F, de: (d: D) => E, cd: (c: C) => D, bc: (b: B) => C, ab: (...a: A) => B): (...a: A) => G;
function compose<A extends unknown[], B, C, D, E, F, G, H>(gh: (g: G) => H, fg: (f: F) => G, ef: (e: E) => F, de: (d: D) => E, cd: (c: C) => D, bc: (b: B) => C, ab: (...a: A) => B): (...a: A) => H;
function compose<A extends unknown[], B, C, D, E, F, G, H, I>(hi: (h: H) => I, gh: (g: G) => H, fg: (f: F) => G, ef: (e: E) => F, de: (d: D) => E, cd: (c: C) => D, bc: (b: B) => C, ab: (...a: A) => B): (...a: A) => I;
function compose<A extends unknown[], B, C, D, E, F, G, H, I, J>(ij: (i: I) => J, hi: (h: H) => I, gh: (g: G) => H, fg: (f: F) => G, ef: (e: E) => F, de: (d: D) => E, cd: (c: C) => D, bc: (b: B) => C, ab: (...a: A) => B): (...a: A) => J;

Example:

const shout = compose((s: string) => `${s}!`, (s: string) => s.toUpperCase());
shout('hi'); // => 'HI!'

constant

Returns a function that always returns value.

function constant<T>(value: T): () => T;

Example:

const alwaysTrue = constant(true);
alwaysTrue(); // => true

identity

Returns its argument.

function identity<T>(value: T): T;

Example:

[1, 0, 2].filter(identity); // => [1, 2]

negate

Inverts a predicate.

function negate<A extends unknown[]>(predicate: (...args: A) => unknown): (...args: A) => boolean;

Example:

[1, 2, 3].filter(negate((n: number) => n % 2 === 0)); // => [1, 3]

once

Calls fn only on the first call; later calls return the first result (or rethrow the first error).

function once<A extends unknown[], R>(fn: (...args: A) => R): (...args: A) => R;

Example:

let setups = 0;
const init = once(() => ++setups);
init();
init(); // => 1 // returns the first result
setups; // => 1

pipe

Left-to-right composition: pipe(f, g)(x) === g(f(x)). The first function may take several arguments.

function pipe<A extends unknown[], B>(ab: (...a: A) => B): (...a: A) => B;
function pipe<A extends unknown[], B, C>(ab: (...a: A) => B, bc: (b: B) => C): (...a: A) => C;
function pipe<A extends unknown[], B, C, D>(ab: (...a: A) => B, bc: (b: B) => C, cd: (c: C) => D): (...a: A) => D;
function pipe<A extends unknown[], B, C, D, E>(ab: (...a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E): (...a: A) => E;
function pipe<A extends unknown[], B, C, D, E, F>(ab: (...a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F): (...a: A) => F;
function pipe<A extends unknown[], B, C, D, E, F, G>(ab: (...a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G): (...a: A) => G;
function pipe<A extends unknown[], B, C, D, E, F, G, H>(ab: (...a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H): (...a: A) => H;
function pipe<A extends unknown[], B, C, D, E, F, G, H, I>(ab: (...a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H, hi: (h: H) => I): (...a: A) => I;
function pipe<A extends unknown[], B, C, D, E, F, G, H, I, J>(ab: (...a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H, hi: (h: H) => I, ij: (i: I) => J): (...a: A) => J;

Example:

const slug = pipe((s: string) => s.trim(), (s) => s.toLowerCase(), (s) => s.replaceAll(' ', '-'));
slug(' Hello World '); // => 'hello-world'

safeCall

Runs fn, swallowing any throw: returns undefined and reports the error to onError (if given). Use for optional user callbacks that must not break the caller.

function safeCall<R>(fn: () => R, onError?: (error: Error) => void): R | undefined;

Example:

const errors: string[] = [];
safeCall(() => { throw new Error('onChange failed.'); }, (e) => { errors.push(e.message); }); // => undefined
errors; // => ['onChange failed.']

tap

Wraps fn to run it for side effects and pass the value through.

function tap<T>(fn: (value: T) => void): (value: T) => T;

Example:

const seen: number[] = [];
const double = pipe((s: string) => Number(s), tap((n: number) => { seen.push(n); }), (n) => n * 2);
double('21'); // => 42
seen; // => [21]

unary

Limits fn to its first argument (for map(parseInt)-style pitfalls).

function unary<T, R>(fn: (value: T, ...rest: never[]) => R): (value: T) => R;

Example:

['1', '2', '3'].map(unary(parseInt)); // => [1, 2, 3]

noop

Does nothing and returns undefined.

const noop: (...args: unknown[]) => void;

Example:

const options: { onChange?: (value: string) => void } = {};
const onChange = options.onChange ?? noop;
onChange('Ada'); // => undefined

memoize

Caches results of fn in a bounded LRU map (default 1000 entries), optionally with a TTL. Throws are not cached; returned promises are (including ones that later reject - clear on failure if needed).

function memoize<A extends unknown[], R>(fn: (...args: A) => R, options?: MemoizeOptions<A>): Memoized<A, R>;

Example:

const fib = memoize((n: number): number => (n < 2 ? n : fib(n - 1) + fib(n - 2)));
fib(50); // => 12586269025
let loads = 0;
const label = memoize((id: string, lang: string) => { loads++; return `${id}:${lang}`; }, { key: (id, lang) => `${id}|${lang}`, ttlMs: 60_000 });
label('EMP-1', 'de');
label('EMP-1', 'de'); // => 'EMP-1:de'
loads; // => 1

memoizeOne

Remembers only the latest call: returns the cached result while the arguments are equal (same length, each Object.is, or a custom isEqual).

function memoizeOne<A extends unknown[], R>(fn: (...args: A) => R, isEqual?: (next: A, prev: A) => boolean): (...args: A) => R;

Example:

interface Row { name: string }
let runs = 0;
const visible = memoizeOne((rows: Row[], filter: string) => { runs++; return rows.filter((r) => r.name.includes(filter)); });
const rows: Row[] = [{ name: 'Ada' }, { name: 'Grace' }];
visible(rows, 'Ad'); // => [{ name: 'Ada' }]
visible(rows, 'Ad'); // same arguments: cached
runs; // => 1

Memoized

A memoized function with cache controls.

interface Memoized<A extends unknown[], R> {
(...args: A): R;
clear(): void;
delete(...args: A): boolean;
readonly size: number;
}

MemoizeOptions

interface MemoizeOptions<A extends unknown[]> {
readonly key?: (...args: A) => unknown;
readonly maxSize?: number;
readonly ttlMs?: number;
readonly now?: () => number;
}

debounce

Delays calling fn until wait ms have passed since the last call (lodash-compatible edges and maxWait). this is not forwarded; wrap methods in an arrow function.

function debounce<A extends unknown[], R>(fn: (...args: A) => R, wait: number, options?: DebounceOptions): Debounced<A, R>;

Example:

const saved: string[] = [];
const save = debounce((text: string) => { saved.push(text); }, 300);
save('D');
save('Dr');
save('Draft');
save.flush(); // runs the pending call now (e.g. on beforeunload)
saved; // => ['Draft']

rafThrottle

Calls fn at most once per animation frame with the latest arguments. Uses requestAnimationFrame when available at call time, otherwise a 16 ms timer (SSR/Node/workers). No global access at import.

function rafThrottle<A extends unknown[]>(fn: (...args: A) => void): FrameThrottled<A>;

Example:

declare const handle: HTMLElement;
const onMove = rafThrottle((e: PointerEvent) => { handle.style.translate = `${e.clientX}px ${e.clientY}px`; });
handle.addEventListener('pointermove', onMove); // at most one update per frame

throttle

Calls fn at most once per wait ms (leading and trailing by default).

function throttle<A extends unknown[], R>(fn: (...args: A) => R, wait: number, options?: ThrottleOptions): Debounced<A, R>;

Example:

let calls = 0;
const onScroll = throttle(() => { calls++; }, 100);
onScroll();
onScroll();
onScroll();
calls; // => 1 // leading call; the others merge into one trailing call
onScroll.cancel();

Debounced

A debounced function with control methods. Returns the result of the last invocation.

interface Debounced<A extends unknown[], R> {
(...args: A): R | undefined;
cancel(): void;
flush(): R | undefined;
pending(): boolean;
}

DebounceOptions

interface DebounceOptions {
readonly leading?: boolean;
readonly trailing?: boolean;
readonly maxWait?: number;
}

FrameThrottled

A frame-throttled function.

interface FrameThrottled<A extends unknown[]> {
(...args: A): void;
cancel(): void;
}

ThrottleOptions

interface ThrottleOptions {
readonly leading?: boolean;
readonly trailing?: boolean;
}