I’m Kayla. I write TypeScript a lot. Coffee at my desk. Git yelling at me sometimes. And I had one small ask: rename a field, but keep the JSDoc clean and safe. If you want the more formal deep-dive version of this same journey, you can check out Renaming a TypeScript Field and Keeping JSDoc: My Hands-On Review.
You’d think it’s simple. It is… mostly. I tried a few real paths on my laptop, and I’ll show you what worked, what broke, and the tiny gotchas.
My setup: macOS Sonoma, VS Code 1.93, TypeScript 5.6.3, Node 20.
The quick win: VS Code “Rename Symbol” (F2)
Here’s the thing. The built-in rename in VS Code is actually great. It uses the TypeScript language service under the hood. So it can track types, not just text. And it keeps your JSDoc right where it belongs. If you’re looking for a broader discussion on how I structure and preserve comments in real projects, I share concrete patterns in Comments in TypeScript—my real-world take.
Fun fact: the quality of this rename operation got a notable bump back in TypeScript 4.5.
If you’re hungry for more hands-on refactoring tactics, the walkthrough on Improving Code expands on these rename strategies with broader TypeScript examples. If you happen to work in JetBrains WebStorm, the IDE offers a dedicated set of TypeScript-specific rename refactorings that feel pretty similar.
Steps I use:
- Put the cursor on the field name (the declaration, not just a random use).
- Press F2. Type the new name. Hit Enter.
- Watch edits roll across files. It’s kinda nice.
Real example: class field
Before:
class User {
/** The user's name shown in the app */
name: string;
constructor(name: string) {
this.name = name;
}
}
const u = new User("Kayla");
console.log(u.name);
I put my cursor on name in the class, hit F2, and rename to fullName.
After:
class User {
/** The user's name shown in the app */
fullName: string;
constructor(fullName: string) {
this.fullName = fullName;
}
}
const u = new User("Kayla Sox");
console.log(u.fullName);
The JSDoc stayed. All uses changed. No drama.
Real example: interface property
Before:
interface Person {
/** Age in years, used for stats and badges */
age: number;
}
const p: Person = { age: 7 };
function celebrate(x: Person) {
return x.age + 1;
}
Rename age to years on the interface.
After:
interface Person {
/** Age in years, used for stats and badges */
years: number;
}
const p: Person = { years: 7 };
function celebrate(x: Person) {
return x.years + 1;
}
Again, the doc stays with the declaration. Usages follow.
Real example: type alias object
Before:
type Product = {
/** Short text shown on cards */
label: string;
};
const card: Product = { label: "Summer Hat" };
console.log(card.label);
Rename label to title.
After:
type Product = {
/** Short text shown on cards */
title: string;
};
const card: Product = { title: "Summer Hat" };
console.log(card.title);
No issues. JSDoc holds.
But wait—here’s where it gets weird
Not everything follows clean. I ran into these corners:
-
Plain object literals with no type don’t rename across uses. This one bit me.
Example:
// Untyped object const config = { /** Show this many items */ count: 10 }; // Later console.log(config.count);If you F2 on
countin the object, VS Code treats it like a local key, not a symbol with references. It changes only that spot. Theconfig.countuse won’t update.Quick fix? Give it a type:
interface Config { /** Show this many items */ count: number; } const config: Config = { count: 10 }; console.log(config.count);Now F2 works across the file tree, and the doc stays with
count. -
Computed names or string index tricks can be hit or miss.
interface Bag { /** Unique key */ id: string; } const key: keyof Bag = "id"; // this updates when I rename "id" const k = "id"; // this does NOT update; it’s just a stringIf it’s a plain string, it won’t track. I learned that the hard way during a Friday refactor. Not fun.
-
Getter/setter pairs keep the doc on the declaration you rename.
class Box { /** Width in px */ get width() { return 100; } set width(v: number) {} }If I rename
widthon the getter, the doc stays with the getter. That’s fine, but it surprised a teammate. We moved the doc to the class field later.
Batch renames at scale: ts-morph script
When I had to rename fields across many files in one sweep, I used ts-morph. It wraps the TypeScript API. It calls the same rename that VS Code uses. So it keeps JSDoc too. Notice the tsConfigFilePath argument in the snippet—if you’re curious about how to juggle those path parameters in tooling, I break it down in TypeScript file/path argument—my hands-on take.
My script:
// scripts/rename-field.ts
import { Project } from "ts-morph";
async function run() {
const project = new Project({ tsConfigFilePath: "tsconfig.json" });
// 1) Class property
for (const sf of project.getSourceFiles()) {
for (const cls of sf.getClasses()) {
const prop = cls.getProperty("name");
if (prop) {
prop.rename("fullName");
}
}
}
// 2) Interface property
for (const sf of project.getSourceFiles()) {
for (const intf of sf.getInterfaces()) {
const prop = intf.getProperty("age");
if (prop) {
prop.rename("years");
}
}
}
await project.save();
}
run().catch(e => {
console.error(e);
process.exit(1);
});
I ran it with ts-node scripts/rename-field.ts. It updated references and kept JSDoc. Clean commit. Big sigh of relief.
Tiny note: if your code has errors, the rename can skip spots. I run tsc --noEmit first.
What failed for me: raw find-and-replace
I tried a quick regex once. Bad idea. It broke comments and kept the JSDoc glued to the old key.
Here’s a small mess I made:
Before:
type Trade = {
/** Time the trade was placed */
placedAt: string;
};
const t: Trade = { placedAt: "2025-01-02T10:00:00Z" };
console.log(t.placedAt);
I did a simple replace of placedAt with createdAt. It hit docs and strings too.
After (oops):
type Trade = {
/** Time the trade was createdAt */
createdAt: string;
};
const t: Trade = { createdAt: "2025-01-02T10:00:00Z" };
console.log(t.createdAt);
The doc is now wrong. It reads weird. And if I had “placedAt” inside a URL or JSON? Yikes. I had to hand-fix lines. You know what? I never went back to that path.
Another real-world bit: destructuring holds up
I wanted to see if destructuring keeps up. It does.
Before:
interface Env {
/** App environment label */
label: string;
/** Build number shown in footer */
build: number;
}
const env: Env = { label: "prod", build: 42 };
const { label } = env;
console.log(label);
Rename label to title on the interface.
After:
“`ts
interface Env {
/** App environment label /
title: string;
/* Build number shown in footer */
build: number;
}
const env: Env = { title: "prod", build: 42 };
const { title } = env
