TypeScript interface type union intersection

[TypeScript] 介面與型別別名

上一篇行內物件型別 { name: string; age: number } 寫多了會崩潰。這一篇用 interfacetype 把型別命名、組合、擴充的工具一次補齊,順便釐清 |&、discriminated union、結構型別等 TS 賴以為生的觀念。

兩種定義方式

// interface
interface User {
  id: number
  name: string
}

// type alias
type User = {
  id: number
  name: string
}

兩者大多時候可互換,差異整理在文末。

屬性修飾

interface User {
  readonly id: number     // 唯讀
  name: string
  email?: string          // 可選
  [key: string]: unknown  // index signature:任意字串 key 都允許
}

? 大致等同 name: string | undefined,差別在它還允許整個欄位直接省略。

extends:繼承

interface Animal {
  name: string
}

interface Dog extends Animal {
  breed: string
}
// Dog = { name: string; breed: string }

// 多重繼承
interface A { a: number }
interface B { b: string }
interface AB extends A, B {}

type&(intersection)達到同樣效果:

type Animal = { name: string }
type Dog = Animal & { breed: string }

intersection(&):合併

type Loggable = { log: () => void }
type Serializable = { serialize: () => string }

type Both = Loggable & Serializable
// 必須同時實作 log 與 serialize

extends 的差異:& 適合組合多個獨立特徵,extends 適合表達「is-a」階層關係。

如果兩邊有同名但型別衝突的屬性:

type A = { x: string }
type B = { x: number }
type C = A & B   // x: never(string 與 number 無交集)

union(|):擇一

type Id = string | number

function format(id: Id) {
  // 直接用 toUpperCase 會錯,因為 number 沒這方法
  if (typeof id === 'string') {
    return id.toUpperCase()
  }
  return id.toString(16)
}

union 的成員可以是任何型別:原始值、字面值、物件、其他 union。

字面值 union:

type Status = 'idle' | 'loading' | 'done'

let s: Status = 'idle'
s = 'pending'   // ❌ 不在允許範圍

discriminated union:帶辨識欄位的 union

union 最好用的模式之一,常拿來表達「這筆資料只會是這幾種狀態之一」:

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string }

function handle(r: Result<number>) {
  if (r.ok) {
    console.log(r.value)    // ✅ TS 知道 value 存在
  } else {
    console.log(r.error)    // ✅ TS 知道 error 存在
  }
}

辨識欄位(這裡是 ok)讓 TS 在 if / switch 後自動窄化分支。型別守衛章節會更深入。

所謂「辨識」,是指 union 裡每個分支都帶一個共同欄位(例如 ok: true | falsekind: 'circle' | 'square')。TS 在 if / switch 裡讀到這個欄位的值,就會自動把變數縮成對應的分支(type narrowing);少了這個共同欄位,TS 就無從判斷當下是哪一種。

函式型別

// 在 interface 裡
interface Greeter {
  (name: string): string                    // call signature
  version: string                           // 也可以同時是物件
  greet(name: string): string               // 方法簽章
}

// type 寫法
type Add = (a: number, b: number) => number
const add: Add = (a, b) => a + b

類別實作 interface

interface Logger {
  log(msg: string): void
}

class ConsoleLogger implements Logger {
  log(msg: string) {
    console.log(msg)
  }
}

implements 只是型別檢查約束,不會繼承實作。要繼承用 extends

介面合併(declaration merging)

interface 同名宣告會自動合併,type 不行:

interface Window {
  myCustomProp: string
}
interface Window {
  anotherProp: number
}
// 合併為 { myCustomProp: string; anotherProp: number; ... }

type Foo = { a: number }
type Foo = { b: string }   // ❌ Duplicate identifier 'Foo'

主要應用:擴充全域型別(例如 WindowExpress.Request)。

interface vs type 對照

能力interfacetype
描述物件結構
extends / &extends&
描述函式 / call signature
union / intersection
字面值 / 原始型別別名
同名合併
對應 mapped type / conditional type
對 IDE 提示效能略快略慢

實務取捨:

  • 物件結構、可能被外部擴充(例如 library 公開的 props)→ interface
  • 需要 union、intersection、字面值、mapped、conditional → type
  • 風格上沒共識,團隊統一就好

結構型別(structural typing)

TS 的型別檢查是結構性的,不是名義性的(不像 Java 要顯式 implements):

interface Point {
  x: number
  y: number
}

const p = { x: 1, y: 2, z: 3 }
const point: Point = p   // ✅ p 結構符合 Point 即可

只看「形狀對不對」,不看「叫什麼名字」。這是 TS 哲學的核心之一。

對比一下:Java / C# 走 nominal typing(名義型別),就算兩個 class 欄位完全一樣,沒有明確 implements 同一個 interface 就不相容;TS 走 structural typing,只看形狀、不看名字。好處是能無痛相容第三方型別;代價是形狀剛好一樣、語意卻不同的東西,也會被當成同一型別。

多餘屬性檢查的例外

物件字面值直接傳進函式時,TS 會多做一層「多餘屬性檢查」(excess property check):

function f(p: Point) {}

f({ x: 1, y: 2, z: 3 })   // ❌ z 不在 Point 上
const obj = { x: 1, y: 2, z: 3 }
f(obj)                     // ✅ 透過變數中轉就 OK

這是 TS 為了抓拼錯屬性名而做的特例檢查。

readonly 對陣列與 tuple

const a: readonly number[] = [1, 2, 3]
a.push(4)         // ❌
a[0] = 9          // ❌

// 等價寫法
const b: ReadonlyArray<number> = [1, 2, 3]

// readonly tuple
const t: readonly [string, number] = ['a', 1]

把陣列宣告成 readonly 是傳函式參數時的好習慣,明示「我不會改」。

物件結構與型別組合搞定,下一篇處理另一個天天碰的東西——函式的型別,包含 overload、this、回呼簽章與 void 的細節。

Latest Updates

  • 2026.06.11 Content updated