TypeScript 型別系統基礎完全指南

TypeScript 入門核心:基本型別、型別推論、interface vs type、union/intersection、型別收窄與斷言

本篇是 TypeScript 系列的基礎篇;進階見泛型與工具型別(同目錄)。


目錄


什麼是 TypeScript?

TypeScript(TS) 是 JavaScript 的超集(superset):在 JS 之上加入靜態型別。所有合法的 JS 都是合法的 TS。

關鍵特性:型別只存在於編譯期,編譯成 JS 後型別會被抹除(type erasure),執行時就是純 JS——型別不影響執行行為。

.ts(含型別)── tsc 編譯 ──► .js(型別被抹除)── 在瀏覽器/Node 執行

為什麼用 TypeScript?

  • 編譯期抓錯:型別不符在開發時就報錯,不必等執行才炸
  • IDE 支援強:自動完成、跳轉、重構更可靠
  • 自我文件化:型別本身就是文件,函式參數/回傳一目了然
  • 大型專案維護性:重構時型別系統會抓出受影響的地方

代價:要寫型別、要編譯步驟、有學習曲線。小型拋棄式腳本未必划算。


TS / JavaScript / Node.js / 框架的關係

初學最常見的困惑:「Node.js、Next.js 是不是 TypeScript?」——不是。它們是不同層次的東西:

  • TypeScript = 語言(怎麼寫):JS + 型別,編譯成 JS 才執行
  • Node.js = 執行環境(runtime)(在哪跑):讓 JS 在瀏覽器外執行
  • Next.js / React = 框架 / 函式庫(用什麼蓋)

三者不同層、可搭配——Node.js、Next.js、React 都能用 TypeScript 來寫;TS 最後都編譯成 JS 交給 Node 或瀏覽器執行。TypeScript 只負責「語言」這一層。

完整的生態分層(各層有哪些語言/執行環境/框架、怎麼組合)見 JavaScript 生態全景


基本型別

let s: string = "hello";
let n: number = 42;           // 整數浮點都是 number
let b: boolean = true;
let u: undefined = undefined;
let nl: null = null;

// 陣列
let arr: number[] = [1, 2, 3];
let arr2: Array<string> = ["a", "b"];   // 泛型寫法,等價

// 元組(Tuple):固定長度、各位置型別固定
let pair: [string, number] = ["age", 30];

// any:放棄型別檢查(盡量避免)
let whatever: any = 1;

// unknown:未知型別,但用前必須先收窄(比 any 安全)
let value: unknown = getData();

// void:函式無回傳值
function log(msg: string): void { console.log(msg); }

// never:永不回傳(拋錯或無窮迴圈)
function fail(msg: string): never { throw new Error(msg); }

型別註記與型別推論

TypeScript 能自動推論型別,不必每處都標:

let x = 10;          // 推論為 number
x = "hi";            // ❌ 錯:不能把 string 指給 number

const name = "Alice"; // 推論為字面值型別 "Alice"(const 更窄)

// 函式回傳值通常可推論,但「參數」一定要標
function add(a: number, b: number) {   // 回傳自動推論為 number
  return a + b;
}

原則:讓 TS 推論能推的,只在它推不出或想更精確時才手動標(尤其函式參數、公開 API 邊界)。


物件型別:interface vs type

兩種定義物件結構的方式,常被問差異:

// interface
interface User {
  id: number;
  name: string;
  email?: string;        // ? = 選填
  readonly createdAt: Date;  // readonly = 唯讀
}

// type alias
type UserT = {
  id: number;
  name: string;
};

差異對照

面向 interface type
物件 / 類別形狀
union / intersection / 基本型別別名 ✅(type X = A | B
宣告合併(同名自動合併)
擴充 extends &(intersection)

經驗法則:物件/類別的形狀、需要被 extends 或宣告合併 → 用 interfaceunion、tuple、需要型別運算 → 用 type。團隊一致即可,兩者多數情況可互換。


Union 與 Intersection

// Union(或):是其中之一
type Id = string | number;
let id: Id = 123;
id = "abc";              // 都可以

// Intersection(且):同時滿足多個
type Named = { name: string };
type Aged  = { age: number };
type Person = Named & Aged;   // 同時要有 name 和 age
const p: Person = { name: "A", age: 30 };
  • Union |:值是「A 或 B」,使用前常需收窄
  • Intersection &:型別是「A 且 B」的合併

字面值型別與 enum

// 字面值型別:限定為特定值
type Direction = "up" | "down" | "left" | "right";
let dir: Direction = "up";
dir = "north";    // ❌ 不在允許值內

// 常用於函式參數,比 enum 輕量
function move(d: "up" | "down") {}

// enum:具名常數集合
enum Status { Active, Inactive, Pending }  // 預設 0,1,2
let st: Status = Status.Active;

實務上字面值 union 常比 enum 更受青睞(更輕量、無執行期產物;enum 會編譯出實際物件)。需要具名常數集合時才用 enum。


函式型別

// 參數與回傳型別
function greet(name: string): string {
  return `Hi ${name}`;
}

// 選填參數、預設值、其餘參數
function build(a: string, b?: string, c = "x", ...rest: number[]) {}

// 函式型別表達式(用於回呼)
type Handler = (event: string) => void;
const h: Handler = (e) => console.log(e);   // e 自動推論為 string

// 箭頭函式
const square = (n: number): number => n * n;

型別收窄(Narrowing)

當值是 union 型別時,TS 會依控制流程自動把型別收窄到更精確的範圍:

function format(x: string | number) {
  if (typeof x === "string") {
    return x.toUpperCase();   // 這裡 x 已收窄為 string
  }
  return x.toFixed(2);        // 這裡 x 已收窄為 number
}

常用收窄手段

手段 用途
typeof x === "string" 基本型別
x instanceof Foo 類別實例
"prop" in x 物件是否有某屬性
真值判斷 if (x) 排除 null / undefined / 0 / ""
可辨識聯合(discriminated union) 用共同的字面值欄位區分

可辨識聯合(很實用)

用一個共同的「標籤」欄位區分 union 成員:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.radius ** 2;  // 收窄為 circle
    case "square": return s.side ** 2;              // 收窄為 square
  }
}

any / unknown / never

三個容易混淆的特殊型別:

型別 意義 用法
any 關閉型別檢查,什麼都能做 盡量避免,等於放棄 TS 的保護
unknown 「未知」,但使用前必須先收窄 接外部/不可信資料的安全選擇
never 不可能有值(永不發生) 窮盡檢查、永遠拋錯的函式
let a: any = 5;
a.foo.bar();          // 不報錯(危險)

let u: unknown = 5;
u.toFixed();          // ❌ 報錯:要先收窄
if (typeof u === "number") u.toFixed();  // ✅

// never:窮盡檢查(漏掉 case 會編譯期報錯)
function assertNever(x: never): never { throw new Error("unexpected"); }

優先用 unknown 取代 any:保留型別安全,逼你在使用前先檢查。


型別斷言

告訴編譯器「我比你更確定這個值的型別」:

const el = document.getElementById("app") as HTMLInputElement;
el.value = "x";        // 斷言成 input 才有 .value

// 非空斷言 !:保證不是 null/undefined
const node = document.querySelector(".btn")!;

⚠️ 斷言是繞過檢查,不是轉換——斷錯了執行期照樣出錯。能用收窄就別用斷言;! 尤其要謹慎,它只是叫 TS 閉嘴。


tsconfig 與 strict 模式

tsconfig.json 控制編譯行為,最重要的是 strict

{
  "compilerOptions": {
    "strict": true,          // 開啟所有嚴格檢查(強烈建議)
    "target": "ES2022",      // 編譯成哪個 JS 版本
    "module": "ESNext",
    "noUncheckedIndexedAccess": true  // 索引存取也要考慮 undefined
  }
}

strict: true 會開啟一系列檢查,最重要的:

  • strictNullChecksnull / undefined 不再能隨意指給其他型別——TS 最有價值的保護之一
  • noImplicitAny:不允許隱含的 any

新專案一律開 strict。關掉等於放棄 TS 大半價值。


最佳實踐

  1. strict,尤其 strictNullChecks
  2. 避免 any,優先 unknown 接不可信資料
  3. 讓 TS 推論能推的,只在邊界(函式參數、公開 API)手動標型別
  4. union + 可辨識聯合 + 收窄,少用型別斷言
  5. 字面值 union 優先於 enum(更輕量)
  6. 斷言與 ! 當最後手段,它們繞過檢查而非保證安全
  7. interface / type 選一套慣例並一致

常見問題

問題 1:interface 和 type 該用哪個?

物件/類別形狀、需要 extends 或宣告合併 → interface;union、tuple、型別運算 → type。多數情況可互換,團隊一致最重要。

問題 2:any 和 unknown 差在哪?

any 完全關閉檢查(危險);unknown 是「未知但安全」——使用前必須先收窄型別。接外部資料優先用 unknown

問題 3:TypeScript 的型別在執行期還在嗎?

不在。型別在編譯時被抹除(type erasure),產出的是純 JS。所以不能在執行期用型別做判斷(要判斷得靠 typeofinstanceof 等執行期手段)。

問題 4:為什麼一定要開 strict?

strict 開啟 strictNullChecks 等關鍵檢查,能抓出大量 null/undefined 錯誤——這正是 TS 最有價值的地方。關掉等於只拿到半套保護。

問題 5:字面值 union 和 enum 怎麼選?

字面值 union("a" | "b")更輕量、無執行期產物,多數情況更受青睞;需要具名常數集合、或要在執行期列舉值時才用 enum。

問題 6:Node.js / Next.js 是 TypeScript 嗎?

不是。TypeScript 是語言Node.js 是執行環境(runtime)Next.js / React 是框架 / 函式庫——三者不同層次。Node.js、Next.js、React 都可以「用 TypeScript 來寫」,但它們本身不是 TS。詳見 TS / JavaScript / Node.js / 框架的關係


總結

核心要點

  • TS = JavaScript + 靜態型別;型別編譯期存在、執行期被抹除
  • 善用型別推論,只在邊界手動標註
  • interface(物件/類別、可合併)vs type(union/tuple/運算)
  • Union |(或,需收窄)/ Intersection &(且,合併)
  • 型別收窄(typeof/instanceof/in/可辨識聯合)讓 union 變精確
  • unknown 優於 anynever 表不可能;斷言/! 是繞過檢查要慎用
  • 新專案一律開 strict

快速參考

概念 重點
any / unknown / never 關閉檢查 / 未知需收窄 / 不可能有值
interface vs type 形狀可合併 vs union/運算
| / & union(或)/ intersection(且)
收窄 typeof / instanceof / in / 可辨識聯合
斷言 as / ! 繞過檢查,慎用
strict 必開,含 strictNullChecks

建立日期:2026-06-18

🔗相關文章