typescript
39 lines · 8 steps
Memoizing async functions with TTL in TypeScript
A generic wrapper caches in-flight promises by argument key and expires them after a configurable time-to-live.
Explained by
highlit
1type AsyncFn<A extends unknown[], R> = (...args: A) => Promise<R>;
2
3interface MemoizeOptions<A extends unknown[]> {
4 keyFn?: (...args: A) => string;
5 ttlMs?: number;
6}
7
8interface CacheEntry<R> {
9 value: Promise<R>;
10 expiresAt: number;
11}
12
13export function memoizeAsync<A extends unknown[], R>(
14 fn: AsyncFn<A, R>,
15 options: MemoizeOptions<A> = {},
16): AsyncFn<A, R> {
17 const { keyFn = (...args) => JSON.stringify(args), ttlMs = Infinity } = options;
18 const cache = new Map<string, CacheEntry<R>>();
19
20 return async (...args: A): Promise<R> => {
21 const key = keyFn(...args);
22 const now = Date.now();
23 const cached = cache.get(key);
24
25 if (cached && cached.expiresAt > now) {
26 return cached.value;
27 }
28
29 const value = fn(...args);
30 cache.set(key, { value, expiresAt: now + ttlMs });
31
32 try {
33 return await value;
34 } catch (err) {
35 cache.delete(key);
36 throw err;
37 }
38 };
39}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Caching the promise itself, not the resolved value, deduplicates concurrent calls before any of them finish.
- 2Deleting the entry when the promise rejects prevents a transient failure from being cached forever.
- 3Generic parameters over the argument tuple and return type keep the wrapper fully type-safe for any async function.
Related explainers
typescript
import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, timer, throwError } from 'rxjs'; import { switchMap, takeWhile, filter, take, catchError } from 'rxjs/operators';
Polling a job until it finishes in Angular
rxjs
polling
observables
Intermediate
7 steps
go
package handler type flightResult struct { status int
Deduping in-flight requests in Gin
middleware
concurrency
deduplication
Advanced
9 steps
typescript
type Flatten = Record<string, unknown>; function isPlainObject(value: unknown): value is Record<string, unknown> { return (
Flattening nested objects into dotted keys
recursion
reduce
type-guards
Intermediate
7 steps
typescript
import { Injectable, signal, computed, effect, inject } from '@angular/core'; import { DOCUMENT } from '@angular/common'; export type Theme = 'light' | 'dark';
A signal-based theme service in Angular
signals
reactivity
dependency-injection
Intermediate
7 steps
javascript
import { useCallback, useRef, useState } from 'react'; export function ColorPicker({ initialColor = '#3b82f6', onCommit }) { const [committed, setCommitted] = useState(initialColor);
A validated color picker in React
uncontrolled-inputs
refs
validation
Intermediate
7 steps
typescript
import { Injectable } from '@angular/core'; import { HttpClient, HttpEventType, HttpRequest } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map, distinctUntilChanged, scan } from 'rxjs/operators';
Tracking upload progress in Angular
rxjs
http-events
state-reduction
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/memoizing-async-functions-with-ttl-in-typescript-explained-typescript-6c38/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.