Refactoring prompts with type-safety locks
The type-safety lock
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. Noas unknown as ...cheats.- Public function signature unchanged.
- Add JSDoc-style
@throwsfor any error path.- Use
readonlyon 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:
| Lock | What the model did | What would have happened without it |
|---|---|---|
No any | Used a generic <T> parameter | Would have typed arr: any[] |
No as unknown as cheats | Clean type flow throughout | Might have cast out to T[][] from any[][] |
| Signature unchanged | Same name, same param order, same param types semantically | Might have flipped arr and size, or renamed |
@throws JSDoc | Added the doc comment with the error condition | Would have left it implicit |
readonly on inputs | Added readonly T[] to arr | Would 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
Unlocked refactor ("clean this up")
- any everywhere — no real type-check
- Silent default values inserted
- Reorders may corrupt partial state on failure
- Reviewer can't tell what changed
Locked refactor (full lock list)
- Generic <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. :::
Sign in to rate