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[].

Or clone locally

git clone -b challenge/2026-09-14 https://github.com/niltonheck/typedrop.git