TypeDrop
2026-08-09 Challenge
2026-08-09
Medium
Typed In-Memory LRU Cache with TTL & Typed Eviction Events
You're building a client-side caching layer for a dashboard that fetches expensive computed metrics from a backend. The cache must evict the least-recently-used entries when it hits capacity, expire entries after a configurable TTL, and emit strongly-typed eviction events so callers can react (e.g. log, re-fetch, or update UI) without casting.
Goals
- Implement a generic `LRUCache<K, V>` class that evicts the least-recently-used entry (using a single insertion-ordered `Map`) when the cache reaches its configured capacity.
- Honour per-entry and default TTLs so that an expired `get` returns `undefined`, removes the entry, and fires an `"expired"` eviction event.
- Emit strongly-typed `EvictionEvent<K, V>` discriminated-union values to all registered listeners for each of the three eviction reasons: `"expired"`, `"evicted"`, and `"deleted"`.
- Declare `DEFAULT_OPTIONS` using the `satisfies` operator so TypeScript verifies it conforms to `LRUCacheOptions` without widening its inferred type.
challenge.ts
export type EvictionReason = "expired" | "evicted" | "deleted";
export type EvictionEvent<K, V> =
| { reason: "expired"; key: K; value: V; expiredAt: number }
| { reason: "evicted"; key: K; value: V }
| { reason: "deleted"; key: K; value: V };
export type EvictionListener<K, V> = (event: EvictionEvent<K, V>) => void;
export interface LRUCacheOptions {
capacity: number;
defaultTTL?: number;
}
// TODO: export const DEFAULT_OPTIONS = { ... } satisfies LRUCacheOptions;
export class LRUCache<K, V> {
constructor(options: LRUCacheOptions) { /* TODO */ }
get(key: K): V | undefined { /* TODO */ }
set(key: K, value: V, ttl?: number): this { /* TODO */ }
delete(key: K): boolean { /* TODO */ }
on(listener: EvictionListener<K, V>): () => void { /* TODO */ }
get size(): number { /* TODO */ }
has(key: K): boolean { /* TODO */ }
clear(): void { /* TODO */ }
}
Hints (click to reveal)
Hints
- A JavaScript `Map` iterates keys in insertion order — deleting a key and re-inserting it moves it to the end, giving you O(1) LRU tracking without a separate linked list.
- Use `Date.now()` when storing an entry (`expiresAt = Date.now() + ttl`) and again inside `get` to compare; store `null` when there is no expiry.
- Narrow `EvictionEvent<K, V>` by switching on `event.reason` — TypeScript will narrow each branch to the exact variant, ensuring you access only the fields that exist on that variant.
Useful resources
Or clone locally
git clone -b challenge/2026-08-09 https://github.com/niltonheck/typedrop.git