
開始在重構舊專案的最近一個月,我每天打開專案程式碼,心情都像是在上墳😖。
這個專案是5年前的舊程式碼,說是用了 TypeScript,但含 any 量高達 80%。
User 是 any,Response 是 any,連 window 也是 (window as any)。
每次寫業務,我都得猜這個 res.data.list 到底是個陣列,還是個 null,還是後端心情不好傳回來的一個空字串。
作為一個有程式碼潔癖的資深前端,我忍不了😡。
每週五下午,趁著業務需求的空窗期,決定搞個大掃除。
目標是:幹掉核心模組裡的所有 any,還 TypeScript 一個明確的類型。
那個週末我是在多巴胺的海洋裡度過的。
我對著介面文件,把那些面目可憎的 any 一個個替換成了極其優雅的 Interface。
Refactor 前:
// 屎山程式碼
function renderUser(user: any) {
const name = user.info.name; // 這裡的 info 可能是 undefined,但 TS 不報錯
const age = user.age.toFixed(2); // 這裡的 age 可能是 string,TS 也不報錯
return `User: ${name}, Age: ${age}`;
}
Refactor 後:
// 優雅程式碼
interface UserInfo {
name: string;
avatar?: string;
}
interface User {
id: number;
info: UserInfo; // 必須存在
age: number; // 必須是數字
}
function renderUser(user: User) {
// 此時 IDE 智能提示全出來了,簡直絲滑
return `User: ${user.info.name}, Age: ${user.age.toFixed(2)}`;
}
看著 VS Code 裡那一個個紅色的波浪線被我修好,看著 TS 編譯通過的綠色對勾,我感覺自己就是程式碼界的上帝🥱。我甚至順手把以前程式碼裡那些醜陋的像 if (user && user.info) 防禦性判斷給刪了—— 因為 Interface 定義裡寫了,info 是必選屬性,不可能為空!
週一早上,我信心滿滿地提了 Merge Request,順便把程式碼推上了測試環境。
我心想:兄弟們,以後你們調介面都有智能提示了😁。
上午 10 點的時候,測試環境崩了。


上午 10 點半,技術總監衝到我工位:你動核心模組了?怎麼列表頁全白了?控制台全是報錯!
我一愣:不可能啊,我本地編譯全通過了,TS 類型檢查也是完美的。
打開控制台一看,我傻眼了。滿屏紅字:
Uncaught TypeError: Cannot read properties of undefined (reading 'name')Uncaught TypeError: user.age.toFixed is not a function
怎麼會?
我明明定義了 interface User,裡面寫死了 info 必須存在,age 必須是 number 啊!
經過兩個小時的狼狽排查,我終於明白了那個讓所有 TS 新手都會摔坑的真理:
TypeScript 的類型,在運行時(Runtime)就是個屁!
Interface 定義裡,我信了後端的文件,寫了 age: number。
但實際上,老數據裡有幾百條數據,age 存的是字串 "18"😡。
以前用 any 的時候,JS 隱式轉換還能跑(或者之前壓根沒調 toFixed)。
現在我為了規範,加了 .toFixed(2),因為 TS 告訴我它是數字。
結果運行時,瀏覽器拿著字串 "18" 去調 toFixed,直接炸穿😥。
Interface 裡我寫了 info: UserInfo(必選)。
於是我自信地刪掉了 if (user.info) 的判空邏輯。
結果後端返回的數據裡,因為歷史髒數據,真的有幾個用戶的 info 是 null😡。
TS 編譯時很開心,瀏覽器運行時直接拋出 Cannot read properties of null。
我把編譯時的類型安全,誤當作了運行時的數據安全。
我以為我在寫 Java,其實我還在寫 JavaScript。TS 編譯成 JS 後,那些漂亮的 Interface 全都被擦除得乾乾淨淨,裸奔的數據依然是那個爛樣子。
any,原來是護身符😥看著回滾後的程式碼,那個醜陋的 any 重新回到了螢幕上,我竟然感到了一絲安全感。
這次事故給了我三個血淋淋的教訓:
別太迷信文檔,要信數據。
後端的 Swagger 文檔寫寫而已,你真信了 Required,上線就得背鍋。
TS 是協定,Zod 才是嚴格的。
如果你真的想保證數據類型安全,光寫 Interface 沒用(那只是給 IDE 看的)。你得上運行時校驗庫(比如 Zod 或 Runtypes)。
// 這才是真安全
const UserSchema = z.object({
age: z.number(), // 運行時如果拿到 string,這裡直接拋錯,而不是等到業務邏輯裡炸開
});
防禦性編程永遠不要刪 !!!
不管 TS 告訴你這個字段多一定存在,只要數據源來自網路(API),老老實實寫 Optional Chaining (user?.info?.name)。
那天下午,我默默地把自己寫的那些 interface 改成了原樣:
interface User {
// 認怂了,全部改成可選 + 聯合類型😒
age?: number | string;
info?: UserInfo | null;
}
雖然程式碼裡又要加回那些煩人的 if 判斷,雖然類型提示沒那麼完美了,但至少——它不崩了😒。
如果你也想重構專案裡的 any,聽哥一句勸:
除非你上了 Zod 做運行時校驗,否則那個醜陋的 any,可能是你專案裡最後一道防線。
分享完畢🙌