@absoluteui/helpers/fn
Debounce, throttle, memoize, once, pipe, and compose.
import { ... } from '@absoluteui/helpers/fn';How to use
Section titled “How to use”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(); // => truesearch.flush();requests; // => ['rao'] // one request, with the last value
search('liam');search.cancel(); // e.g. in disconnectedCallbacksearch.pending(); // => falserequests.length; // => 1How 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; // => 1await managerOf('E-404', '2026-10-01').catch(() => 'failed'); // => 'failed'managerOf.size; // => 1 // the failure was not keptHow 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; // => truebyDept(rows, 'finance') === first; // => falsebyDept([...rows], 'hr') === first; // => false // a new array is a new inputHow 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; // => 2loads; // => 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)); // => undefinedlogged; // => ['Handler failed']What goes wrong?
Section titled “What goes wrong?”debounceandthrottledo not forwardthis: wrap methods in an arrow function (debounce(() => this.save(), 300)).throttleruns the first call immediately and merges the rest into one trailing call; pass{ leading: false }to wait first.memoizekeys by the first argument unless you passkey, 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}` }API reference
Section titled “API reference”General
Section titled “General”Functions
Section titled “Functions”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; // => falsebefore
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); // => 5callIf(false, add, 2, 3); // => undefinedcompose
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(); // => trueidentity
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 resultsetups; // => 1pipe
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); }); // => undefinederrors; // => ['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'); // => 42seen; // => [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]Constants
Section titled “Constants”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'); // => undefinedFunctions
Section titled “Functions”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); // => 12586269025let 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; // => 1memoizeOne
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: cachedruns; // => 1Interfaces
Section titled “Interfaces”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;}Timing
Section titled “Timing”Functions
Section titled “Functions”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 framethrottle
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 callonScroll.cancel();Interfaces
Section titled “Interfaces”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;}