Verified against Cursor · 2026-08-12
Pin Cursor's Agent mode to a genuinely typed utility, not one that leans on any to get through the edge cases
A short, direct brief for a single TypeScript utility function or module that states its exact signature, generic constraints, and edge-case behavior up front, and forbids any or an unnarrowed unknown as a way to dodge a hard case, so the function actually gives callers the safety a utility is supposed to provide.
The prompt
Ready to copy — highlighted parts are example details you can swap.
Write groupByKey in this codebase's utilities. SIGNATURE function groupByKey<T, K extends keyof T>(items: readonly T[], key: K): Map<T[K], T[]> WHAT IT DOES Groups an array of objects into a Map keyed by the value at the given property, preserving original insertion order within each group. EDGE CASES AND THEIR EXACT BEHAVIOR Empty input array returns an empty Map, not undefined. A key value of undefined or null on some items still groups them together under that exact value, rather than being dropped from the result. Generic constraints must be real, not decorative — if the signature says a generic extends a specific shape, every code path has to actually rely on that constraint being true, not silently assume something stronger and hope callers happen to satisfy it. Do not widen an input or return type to any, or to unknown without a narrowing check, as a way to get past a case the honest type would make awkward to express — if the correct type is genuinely hard to express, say so and propose the closest correct alternative (a discriminated union, an overload, a generic with a default) rather than quietly reaching for any. WHAT NOT TO DO - Do not return a looser type than the signature promises "just in case" — returning T | undefined where the signature says T is a silent contract change, not a safety net. - Do not add a runtime check for an edge case and then leave the type signature unchanged, so the compiler still tells callers a case can't happen that the runtime code clearly handles. - Do not use a type assertion to make an edge case compile instead of actually handling it — an assertion papering over a real mismatch is a lie the compiler is now required to repeat to every caller. - Do not write this as a generic-looking helper that's actually only ever going to be called with one concrete type in practice — a false generic adds indirection without adding real reuse. WHEN DONE For each edge case listed above, quote the line of code that handles it and confirm the return type at that line matches the signature exactly. If any edge case turns out to need a change from the signature stated in SIGNATURE, say so and explain why, rather than forcing the original signature to fit anyway.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
When a generic constraint or an edge case makes a signature genuinely awkward to satisfy, the path of least resistance for a model trying to get code to typecheck quickly is to reach for any, an unnarrowed unknown, or a type assertion — all three make the compiler stop complaining without actually resolving the mismatch, which defeats the entire purpose of writing a typed utility in the first place, since the bug the type system would have caught at the call site simply gets relocated to a runtime failure nobody is warned about. Explicitly banning that shortcut while simultaneously naming real alternatives — a discriminated union, an overload, a generic with a default — matters because a bare prohibition with no replacement just pushes the model toward a different, quieter version of the same shortcut; giving it an actual escape hatch that preserves safety changes what it reaches for under the same time pressure that would otherwise produce an any. The rule against a return type that's "loosened just in case" targets a related but distinct failure mode: an overcautious T | undefined return preemptively defends against every edge case at once by pushing a null-check onto every single caller forever, rather than resolving the specific case inside the utility once, which is the entire reason to centralize logic in a utility instead of duplicating a check everywhere it's used. The instruction to catch a runtime check whose type signature was never updated addresses a drift pattern that's especially common in agent-driven edits across a longer session: the model edits the implementation to handle a case it discovers partway through, but the signature was already mentally locked in from the start of the task and doesn't get revisited, leaving the compiler asserting something the runtime code visibly contradicts.
What you get back
function groupByKey<T, K extends keyof T>(items: readonly T[], key: K): Map<T[K], T[]> { const result = new Map<T[K], T[]>() for (const item of items) { const k = item[key] const group = result.get(k) if (group) group.push(item) else result.set(k, [item]) } return result } // Empty input: loop never runs, returns an empty Map — matches SIGNATURE (Map<T[K], T[]>, never undefined). // null/undefined key values: Map.get/set treat them as valid keys directly, so items group correctly under that exact value — no widening to any was needed for this case.
Verified against
Cursor 2.2 · 2026-08-12
Changelog
- 2026-08-12 — Initial publish, verified against Cursor 2.2 Agent mode on a strict-mode TypeScript utility.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
