interface Flyable {
    void fly();
}
 
class Duck {
    public void fly() { /* ... */ }
    public void quack() { /* ... */ }
}
 
void makeItFly(Flyable thing) {
    thing.fly();
}
 
makeItFly(new Duck());  // 編譯錯誤:Duck 沒有宣告 implements Flyable

Duck 明明有 fly() 方法,Java 還是不讓你用——因為 Duck 沒有明確宣告 implements Flyable

換成 TypeScript:

interface Flyable {
    fly(): void;
}
 
class Duck {
    fly() { /* ... */ }
    quack() { /* ... */ }
}
 
function makeItFly(thing: Flyable) {
    thing.fly();
}
 
makeItFly(new Duck());  // OK,Duck 有 fly(),結構相符就好

這個差異,是 Nominal typingStructural typing 的核心區別。


Nominal Typing(名義型別)

型別相容性由名字(和明確的繼承/實作關係)決定

Java、C#、Swift(部分)、Kotlin——Nominal typing 的主要使用者。

class Cat {
    public void makeSound() { System.out.println("meow"); }
}
 
class Dog {
    public void makeSound() { System.out.println("woof"); }
}
 
// Cat 和 Dog 結構相同(都有 makeSound()),但是不同型別
// 你不能把 Cat 用在需要 Dog 的地方,即使結構一樣

Nominal typing 的優點在三個方面:意圖明確,你必須明確宣告「這個型別實作了這個介面」,讓閱讀程式碼的人知道設計者的意圖;防止意外相容,兩個結構相同但語意完全不同的型別不會意外互換(UserIDOrderID 都是 int,但在 nominal 系統裡可以是不同型別);工具支援好,IDE 的 find-all-implementations 在 nominal 系統下是精確的。


Structural Typing(結構型別)

型別相容性由結構(有哪些屬性和方法)決定,和名字無關。

TypeScript、Go、OCaml——Structural typing 的主要使用者。

interface Point2D {
    x: number;
    y: number;
}
 
interface Named {
    x: number;
    y: number;
}
 
// Point2D 和 Named 是不同的 interface 名字,但結構相同
// TypeScript 認為它們是相容的

這讓你在不修改已有型別的情況下,讓它滿足新的介面需求:

// 第三方 library 的型別,你不能修改
class ThirdPartyLogger {
    log(message: string): void { /* ... */ }
    warn(message: string): void { /* ... */ }
}
 
// 你自己定義的介面
interface Logger {
    log(message: string): void;
}
 
function setup(logger: Logger) { /* ... */ }
 
setup(new ThirdPartyLogger());  // 可以!ThirdPartyLogger 結構相符

在 Java 裡,你需要建一個 adapter class 讓 ThirdPartyLogger implements Logger。在 TypeScript 裡,直接用就好。


Go 的 Implicit Interface(隱式介面)

Go 是 structural typing 的典型:

type Writer interface {
    Write(p []byte) (n int, err error)
}
 
// bytes.Buffer 從來沒有宣告「我實作了 Writer」
// 但它有 Write 方法,所以它自動滿足 Writer interface
var w Writer = &bytes.Buffer{}  // 合法

Go 的設計哲學:介面應該在消費者(使用介面的那一方)定義,而不是在實作者(提供方法的那一方)宣告。這讓介面的粒度保持小,更靈活。

// 你的 package 需要一個「能寫入的東西」,你定義這個介面:
type Writable interface {
    Write(p []byte) (n int, err error)
}
 
// os.File、bytes.Buffer、net.Conn、任何有 Write 方法的東西,都自動滿足
// 不需要任何人回頭去修改那些型別

Duck Typing 和 Structural Typing 的關係

Dynamic duck typing(Python)和 Structural typing(TypeScript/Go)解決的是同一個問題——「你有這個方法就能用」——但時機不同:

  • Duck typing(動態):在執行時判斷,如果沒有那個方法,runtime error
  • Structural typing(靜態):在編譯時判斷,如果沒有那個方法,編譯錯誤

Structural typing 是「把 duck typing 的彈性帶進靜態型別系統」的方案。


後端 API 設計的實際影響

Nominal typing 讓 API 邊界更清晰

當你設計一個需要多個 team 使用的內部 API,nominal typing 強迫每個使用方明確宣告「我實作了這個介面」,讓依賴關係顯式且可追蹤。這在大型 codebase 裡是優點,你能精確知道誰依賴了什麼。

Structural typing 讓 testing 更容易

測試時你常常需要一個「假的資料庫」。在 Go 裡,你的 UserRepository interface 只要有對應的方法,任何 struct 都能用,不需要明確宣告 implements

type UserRepository interface {
    FindByID(id int) (User, error)
}
 
// 測試用的假實作
type mockUserRepository struct {
    users map[int]User
}
 
func (m *mockUserRepository) FindByID(id int) (User, error) {
    // 不需要寫 implements UserRepository
    // 只要方法簽名符合,就是 UserRepository
}

在 Java 裡,你的 mock class 要明確 implements UserRepository——小事,但在 structural typing 下這個步驟根本不需要。


選擇上的實際差異

大多數後端工程師不會因為 structural vs nominal 的選擇而換語言。這個區別影響的是:

  1. 你設計介面的心態:nominal 讓你從「這個型別提供什麼」出發設計;structural 讓你從「這個消費者需要什麼」出發設計
  2. 整合第三方程式碼的難度:structural typing 讓整合更輕鬆,不需要 adapter wrapper
  3. 跨 package 的依賴可見性:nominal 更明確,structural 更隱式(好壞取決於你的 codebase 大小)

下一篇:Generics 跨語言