Refactoring prompts with type-safety locks

The type-safety lock

5 min read

The most common refactor that goes wrong is the JavaScript-to-TypeScript port. The model adds types, but it adds the easiest types — any everywhere, or unknown cast back to whatever shape the model assumed. The result compiles. It does not type-check anything meaningful.

The fix is to lock the type system directly in the prompt:

Refactor this JavaScript to TypeScript. Hard constraints:

  • No any. No as unknown as ... cheats.
  • Public function signature unchanged.
  • Add JSDoc-style @throws for any error path.
  • Use readonly on inputs that are not mutated.
function chunk(arr, size) {
  if (size <= 0) throw new Error("size must be > 0");
  const out = [];
  for (let i = 0; i < arr.length; i += size) {
    out.push(arr.slice(i, i + size));
  }
  return out;
}

Output ONLY the TypeScript code block.

Claude Sonnet 4.5 returned:

/**
 * Splits an array into chunks of the specified size.
 * @throws {Error} If size is less than or equal to 0
 */
function chunk<T>(arr: readonly T[], size: number): T[][] {
  if (size <= 0) throw new Error("size must be > 0");
  const out: T[][] = [];
  for (let i = 0; i < arr.length; i += size) {
    out.push(arr.slice(i, i + size));
  }
  return out;
}

Captured from Claude Sonnet 4.5 (claude-sonnet-4-5) on 2026-04-27. Re-runs may differ slightly.

Walk through what the lock list achieved:

LockWhat the model didWhat would have happened without it
No anyUsed a generic <T> parameterWould have typed arr: any[]
No as unknown as cheatsClean type flow throughoutMight have cast out to T[][] from any[][]
Signature unchangedSame name, same param order, same param types semanticallyMight have flipped arr and size, or renamed
@throws JSDocAdded the doc comment with the error conditionWould have left it implicit
readonly on inputsAdded readonly T[] to arrWould have left arr: T[], allowing mutation

Notice what didn't change. The function body — the actual logic — is byte-identical to the JavaScript version. The exception is thrown with the same message under the same condition. There is no new validation, no new defaults, no new imports. The lock list specifically prevented all of those silent regressions.

A subtle benefit: the <T> generic was not in the lock list, but the model added it as the only way to satisfy "no any" while keeping the function useful. The locks force the model toward correct decisions even when it isn't told the exact shape.

Side-by-side: what the same JS-to-TS port looks like with and without the lock list:

Unlocked vs locked refactor output

Risky

Unlocked refactor ("clean this up")

any cheatsarr: any[]
Signature preservedSometimes
Error semanticsMay be reordered
Mutation safetyNo readonly added
Cons
  • any everywhere — no real type-check
  • Silent default values inserted
  • Reorders may corrupt partial state on failure
  • Reviewer can't tell what changed
Safe

Locked refactor (full lock list)

any cheats0
Signature preservedByte-identical
Error semanticsSame throw, same condition
Mutation safetyreadonly T[] on inputs
Pros
  • Generic &#x3C;T> emerges naturally — locks force correct shape
  • Error path stays in original position
  • JSDoc @throws annotation added
  • Diff contains only the type additions

Reusable type-safety lock template, for any TS refactor:

- No `any`. No `as unknown as ...` cheats.
- Public function signature unchanged.
- Add JSDoc-style `@throws` for any error path.
- Use `readonly` on inputs that are not mutated.
- No new runtime validation; preserve original error semantics.
- No new imports.

Save it. The first three lines alone catch 80% of refactor regressions. The full set catches close to 100%.

Next up: scaling this discipline up to multi-file migrations. :::

Quiz

Module 3: Refactoring Prompts

Take Quiz
Was this lesson helpful?

Sign in to rate

FREE WEEKLY NEWSLETTER

Stay on the Nerd Track

One email per week — courses, deep dives, tools, and AI experiments.

No spam. Unsubscribe anytime.