TypeDrop

2026-07-25 Challenge

2026-07-25 Medium

Typed LRU Cache with TTL & Typed Eviction Callbacks

You're building the in-process caching layer for a high-traffic API server. Cached entries must expire after a configurable TTL, the cache must evict the least-recently-used entry when it reaches capacity, and callers need strongly-typed eviction callbacks so downstream systems can react (e.g. flush to disk, emit metrics) without losing the shape of the evicted value.

Goals

  • Define a three-variant discriminated union `EvictionReason` and wire it into a generic `EvictionCallback<K,V>` type.
  • Implement `createLRUCache<K,V>` backed by a `Map`, using the delete-and-reinsert trick to maintain LRU order without a doubly-linked list.
  • Correctly handle TTL expiry in `get()` and `has()` — firing `onEvict` with the `ttl` variant only on an actual cache read, not on a peek.
  • Return entries in MRU-first order from `keys()`, filtering out any stale (expired) entries.
challenge.ts
// Key types and main factory signature — enough to understand the challenge

export type EvictionReason =
  | { kind: "capacity" }
  | { kind: "ttl"; expiredAt: number }
  | { kind: "manual" };

export type EvictionCallback<K, V> = (
  key: K,
  value: V,
  reason: EvictionReason
) => void;

export type LRUCacheOptions<K, V> = {
  capacity: number;
  ttlMs?: number;
  onEvict?: EvictionCallback<K, V>;
};

export interface ILRUCache<K, V> {
  set(key: K, value: V): void;
  get(key: K): V | undefined;
  has(key: K): boolean;
  delete(key: K): boolean;
  clear(): void;
  readonly size: number;
  keys(): K[];
}

// TODO: implement this 👇
export function createLRUCache<K, V>(
  options: LRUCacheOptions<K, V>
): ILRUCache<K, V> {
  throw new Error("Not implemented");
}
Hints (click to reveal)

Hints

  • A plain `Map<K, CacheEntry<V>>` preserves insertion order — deleting a key and re-inserting it on every access is all you need to implement LRU without any extra data structure.
  • For the `EvictionReason` discriminated union, the `kind` property is your type-narrowing tag — make sure every call site constructs the exact variant object (e.g. `{ kind: 'ttl', expiredAt: Date.now() }`).
  • `has()` must NOT call `get()` internally — it should peek without updating `lastUsedAt`, so implement it by reading the Map directly and checking the TTL manually.

Or clone locally

git clone -b challenge/2026-07-25 https://github.com/niltonheck/typedrop.git