TypeDrop
2026-09-14 Challenge
2026-09-14
Hard
Typed LRU Cache with TTL Eviction & Generational Stats
You're building the in-process caching layer for a high-throughput API server. The cache must support generic key-value storage with a maximum capacity (LRU eviction), per-entry time-to-live (TTL) expiry, and a typed snapshot of runtime statistics — all enforced by the compiler with zero `any` or unsafe casts.
Goals
- Implement LRUCache<K, V> with O(1) get/set/delete using a Map-based doubly-linked list (or insertion-order Map trick) that correctly promotes entries on access.
- Enforce TTL expiry in both get() and peek(), recording expirations in stats and distinguishing them from LRU evictions.
- Fire the onEvict callback exclusively on capacity-overflow eviction, never on manual delete or TTL expiry.
- Implement the generic memoize() function using variadic tuple Args so the compiler enforces argument and return-type consistency end-to-end.
challenge.ts
// Key types you'll work with:
type CacheKey<K> = K & { readonly __brand: "CacheKey" };
interface CacheEntry<V> {
value: V;
expiresAt: number | undefined;
insertedAt: number;
}
interface CacheStats {
readonly hits: number;
readonly misses: number;
readonly evictions: number;
readonly expirations: number;
readonly currentSize: number;
}
interface CacheOptions<K, V> {
capacity: number;
defaultTtlMs?: number;
onEvict?: (key: K, value: V) => void;
}
// Main class signature:
export class LRUCache<K, V> {
constructor(options: CacheOptions<K, V>);
get(key: K): V | undefined;
set(key: K, value: V, ttlMs?: number): void;
delete(key: K): boolean;
clear(): void;
peek(key: K): V | undefined;
stats(): CacheStats;
}
export function memoize<Args extends readonly unknown[], R>(
fn: (...args: Args) => R,
cache: LRUCache<string, R>,
keyFn: (...args: Args) => string
): (...args: Args) => R;
Hints (click to reveal)
Hints
- JavaScript's built-in Map preserves insertion order — deleting and re-inserting a key moves it to the 'end', giving you MRU promotion without a custom linked list.
- The branded CacheKey<K> only needs a cast in the single normalise() private method; every other internal path receives CacheKey<K> already, keeping the rest of the code cast-free.
- For memoize, the constraint `Args extends readonly unknown[]` lets you spread args into both fn and keyFn without losing their tuple types — don't widen to unknown[].
Useful resources
Or clone locally
git clone -b challenge/2026-09-14 https://github.com/niltonheck/typedrop.git