TypeDrop
2026-08-17 Challenge
2026-08-17
Hard
Typed LRU Cache with TTL, Generics & Branded Keys
You're building the in-process caching layer for a multi-tenant SaaS platform. Every service — user sessions, feature flags, rate-limit counters — needs a cache that evicts the least-recently-used entry when full, respects per-entry TTLs, and enforces that keys are branded strings so callers can't accidentally mix cache namespaces at the type level.
Goals
- Define `Brand<Base, Tag>` and three concrete branded key types (`SessionKey`, `FeatureFlagKey`, `RateLimitKey`) so the compiler rejects mixing cache namespaces.
- Implement `LRUCache<K extends string, V>` with O(1) doubly-linked-list eviction, per-entry TTL, and a `CacheResult<V>` discriminated union returned from `get()`.
- Track hit, miss, expiration, and eviction counters in `CacheStats` and expose them via a `Readonly` snapshot from `stats()`.
- Implement `createNamespacedCaches()` with a mapped return type that preserves the branded key type and value type inferred from each entry in the options map.
challenge.ts
// Key types & cache result — core of the challenge
type Brand<Base, Tag> = Base & { readonly __tag: Tag };
type SessionKey = Brand<string, "SessionKey">;
type FeatureFlagKey = Brand<string, "FeatureFlagKey">;
type CacheResult<V> =
| { status: "hit"; value: V }
| { status: "miss" }
| { status: "expired"; key: string };
interface CacheEntry<V> {
value: V;
expiresAt: number | null;
prev: CacheEntry<V> | null;
next: CacheEntry<V> | null;
}
// Main class signature — your task is to implement it
class LRUCache<K extends string, V> {
constructor(options: LRUCacheOptions<K, V>) { /* … */ }
set(key: K, value: V, opts?: SetOptions): void { /* … */ }
get(key: K): CacheResult<V> { /* … */ }
delete(key: K): boolean { /* … */ }
peek(key: K): V | undefined { /* … */ }
stats(): Readonly<CacheStats> { /* … */ }
size(): number { /* … */ }
}
Hints (click to reveal)
Hints
- For `Brand<Base, Tag>`, intersect `Base` with `{ readonly __tag: Tag }` — the `__tag` property never exists at runtime but makes each brand structurally distinct to the compiler.
- For the doubly-linked-list, maintain a sentinel `head` (most-recent) and `tail` (least-recent) node; `moveToHead` and `removeTail` give you O(1) `get` and eviction.
- For `createNamespacedCaches`, look at how mapped types can index into `M` with `M[NS]` and use helper types to extract the `K` and `V` type parameters from an `LRUCacheOptions<K, V>`.
Useful resources
Or clone locally
git clone -b challenge/2026-08-17 https://github.com/niltonheck/typedrop.git