2012 年,Microsoft 內部有一個問題:

他們有幾個大型的 JavaScript 應用——Visual Studio 的 web 版、Bing 地圖——幾十萬行 JavaScript。這些 codebase 裡,沒有人知道一個函式的參數到底應該傳什麼進去。你改了一個物件的結構,你不知道哪 300 個地方會在 runtime 炸掉。

TypeScript 是 Microsoft 的答案,在 2012 年底發布。

但要理解為什麼型別系統是一個值得追蹤的演進主題,要從更早的地方開始看。


動態型別的黃金年代(1990 年代末–2000 年代末)

Python、Ruby、PHP、JavaScript——這一代的語言都選擇了動態型別。

原因是合理的:

  1. 快速迭代:你不需要宣告型別,寫起來快,prototype 快,改起來快。
  2. 門檻低x = 5; x = "hello" 不需要解釋什麼是型別宣告。
  3. 靈活:同一個函式可以接受不同型別的輸入,duck typing 讓程式碼在大部分情況下「有點複用性」。

對早期的 web 開發,這些優點是真實的。一個五頁的 PHP 應用,動態型別完全夠用,甚至更快。

這個假設開始崩潰的時間點:當 codebase 從五個檔案變成五百個檔案,從一個人維護變成一個 team 維護。


動態型別在大型 codebase 撞的牆

問題一:函式合約變成隱性知識

def process_payment(amount, currency, user):
    # amount 是 float 還是 Decimal?
    # currency 是 "USD" 還是 {"code": "USD", "symbol": "$"}?
    # user 是 user_id 還是 User 物件?

這個函式三個月前你寫的。你現在要呼叫它,你不確定傳什麼。你去找文件——沒有文件。你去讀原始碼——原始碼呼叫了另外五個函式,每個的輸入型別也不清楚。

動態型別把「這個函式接受什麼」從「程式碼裡明確表達的事」變成「只有原作者知道的隱性知識」。

問題二:重構是賭博

靜態語言重構:你改了一個型別,IDE 告訴你 47 個地方要更新,你一個一個改,編譯通過,你知道改完了。

動態語言重構:你改了一個物件的結構,你祈禱你有好的測試覆蓋率,然後 deploy 到 staging,然後繞著應用點一遍看哪裡炸。沒有炸——可能你改到的地方沒有測試。

問題三:工具支援受限

IDE 的 autocomplete、go-to-definition、refactoring tools——這些功能的基礎是「IDE 知道這個變數是什麼型別」。動態型別讓 IDE 只能猜,猜錯了 autocomplete 建議無效,go-to-definition 跳到錯誤的地方。


靜態型別的早期問題(Java 時代)

靜態型別不是沒有代價的。Java 的早期批評,很多是有道理的:

// 2005 年的 Java,你要這樣寫一個字串 List:
List<String> names = new ArrayList<String>();

右邊的 new ArrayList<String>() 重複了一次型別資訊,完全是廢話(Java 7 才加入 diamond operator <> 解這個問題)。

更嚴重的是:Java 的型別系統是 nominal typing(名義型別)——兩個型別要相容,它們必須有明確的繼承或實作關係。你有一個 Duck class,它有 fly()quack() 方法,但它不是 Bird 的子類別——Java 不讓你把它用在需要 Bird 的地方,即使它的行為完全相容。

這讓靜態型別在某些場景顯得笨拙,需要大量的 boilerplate 來建立類別關係。


漸進式型別(Gradual Typing)的出現

解法不是「一刀切換到靜態型別」,而是可以混用,逐步引入

TypeScript(2012):JavaScript 的超集合,你可以逐步加型別。現有的 JavaScript 檔案不需要改,新寫的部分可以加型別。型別標注是可選的,沒標的地方推斷成 any

Python Type Hints(PEP 484,3.5+):不強制,但可以標。mypypyright 在 CI 時靜態分析。運行時行為不變。

Kotlin(2011,JVM):比 Java 更好的型別推斷,不需要到處重複型別宣告,但保留靜態型別的所有優點。

這個方向的核心主張:靜態型別是一種工具,不是一種信仰。你可以在值得的地方加,不值得的地方不加。


TypeScript 的 Structural Typing 解決了 Java 的 Nominal 問題

TypeScript 選擇了 structural typing(結構型別)——兩個型別只要結構相容(有相同的屬性和方法),就可以互換,不需要明確繼承:

interface Flyable {
  fly(): void;
}
 
class Duck {
  fly() { /* ... */ }
  quack() { /* ... */ }
}
 
function makeItFly(thing: Flyable) {
  thing.fly();
}
 
makeItFly(new Duck());  // OK,Duck 有 fly(),就算它沒有宣告 implements Flyable

這讓 TypeScript 的型別系統比 Java 靈活得多,更接近動態語言的 duck typing,但在編譯時就能抓到不兼容的地方。


現在的狀態:型別系統的共識

2025 年,型別系統的演進已經有了相對清晰的共識:

  • 新語言幾乎都選靜態型別:Go、Rust、Swift、Kotlin、TypeScript——沒有主流的新語言在大型應用場景選純動態型別
  • 動態語言都在補型別標注:Python 有 type hints + mypy,PHP 有 PHPStan,Ruby 有 Sorbet,都在往「可選靜態型別」方向走
  • 大型 JavaScript 專案幾乎都用 TypeScript:Airbnb、Slack、Microsoft 內部——純 JavaScript 在大型 codebase 裡已經是技術債的選擇,不是正常選擇

這不是靜態型別「贏了」。是動態型別在大型、多人、長期維護的 codebase 裡持續撞牆,讓工具和語言設計師不得不往靜態的方向補。


型別系統設計的三個維度

學一個新語言的型別系統,有三件事值得先確認。

第一是它在哪個階段抓錯——編譯時還是 runtime?這決定了錯誤多早被發現、開發時有沒有 IDE 支援。第二是型別的相容性怎麼判斷——結構相符就好(TypeScript、Go 的 structural typing)還是要明確繼承或宣告(Java、C# 的 nominal typing)?這決定了你整合第三方程式碼的難度、以及介面設計的靈活程度。第三是型別推斷能力——你需要到處手動標(Java 早期),還是編譯器能推斷大部分(TypeScript、Rust)?這決定了靜態型別實際帶來多少 boilerplate。

三個問題合起來,決定了這個語言的型別系統在保護你到什麼程度的同時,讓你多費多少力氣。

下一篇:跨語言概念演進的驅動力