cv.cm

編碼 · 程式語言

TypeScript 入門教學

閱讀約 8 分鐘

簡答

TypeScript 是加上靜態型別的 JavaScript。你可以加入 let count: number = 0 這樣的型別標註,編譯器 tsc 會先檢查型別,再把程式碼轉成可在任何瀏覽器或 Node.js 執行的純 JavaScript。核心概念包括基本型別、用來描述物件結構的介面(interface)與型別別名(type alias)、搭配型別縮小(narrowing)的聯集型別,以及讓程式碼可重複使用的泛型。型別在執行階段會被移除。

開啟JSON 格式化

TypeScript 是什麼,用來做什麼

TypeScript 是由 Microsoft 開發的開源語言,於 2012 年首次發布。所有合法的 JavaScript 程式也都是合法的 TypeScript,因此你可以一個檔案一個檔案地逐步導入。型別檢查器能在你輸入時就抓出拼錯屬性名稱或漏掉 null 檢查等錯誤,編輯器也會利用型別提供自動完成與安全的重新命名。它廣泛用於大型前端應用(Angular 就是以它撰寫,React、Vue 與 Svelte 也有良好支援)、Node.js 後端與函式庫。

安裝並執行 Hello, world

先安裝 Node.js,再用 npm 在專案中安裝編譯器。tsc 會把 .ts 檔編譯成 .js。

const message: string = "Hello, world";
console.log(message);
npm install --save-dev typescript
npx tsc --init          # creates tsconfig.json
npx tsc hello.ts        # writes hello.js
node hello.js
# Hello, world

較新版本的 Node.js、Deno 與 Bun 也可以直接執行 .ts 檔,做法是移除型別標註。它們不會做型別檢查,所以仍要執行 npx tsc --noEmit 來找出型別錯誤。

基本型別與型別推論

你很少需要把每個型別都寫出來。初始化變數時,TypeScript 會推論其型別:let n = 5 就是 number。請替函式參數與公開 API 加上型別標註。

型別範例說明
numberlet n: number = 3.5整數與浮點數
stringlet s: string = "hi"文字
booleanlet ok: boolean = truetrue 或 false
陣列number[] 或 Array<number>兩種寫法意思相同
Tuple(元組)[string, number]固定長度與位置
unknownlet x: unknown任何值,使用前必須先檢查
anylet y: any關閉型別檢查,應避免使用
void / never函式回傳型別不回傳任何東西/永遠不會回傳
let count = 0;          // inferred as number
count = "ten";          // error: Type 'string' is not assignable to type 'number'
const names: string[] = ["Ann", "Bo"];
const pair: [string, number] = ["age", 31];

第二行是刻意寫成的編譯錯誤,用來示範檢查器會回報什麼。

函式、流程控制與型別縮小

流程控制(if、for、while、switch)與 JavaScript 完全相同。TypeScript 多出來的是型別縮小:經過 typeof x === "string" 這類檢查後,編譯器就知道該分支內更精確的型別。

function greet(name: string, greeting = "Hello"): string {
  return `${greeting}, ${name}!`;
}
function formatId(id: string | number): string {
  if (typeof id === "number") {
    return id.toFixed(0).padStart(6, "0"); // id is number here
  }
  return id.toUpperCase();                 // id is string here
}
console.log(greet("Lin"));    // Hello, Lin!
console.log(formatId(42));    // 000042
console.log(formatId("ab7")); // AB7

string | number 是聯集型別(union type):值是所列型別之一。選用參數寫成 name?: string,其型別包含 undefined。

介面與型別別名

用 interface 或 type 別名描述物件的結構。TypeScript 檢查的是結構,而不是名稱:只要物件具備正確的屬性就符合。

interface User {
  id: number;
  name: string;
  email?: string;          // optional
  readonly createdAt: Date;
}
type Role = "admin" | "editor" | "viewer"; // string literal union
function describe(user: User, role: Role): string {
  const contact = user.email ?? "no email";
  return `${user.name} (${role}, ${contact})`;
}
const ann: User = { id: 1, name: "Ann", createdAt: new Date() };
console.log(describe(ann, "editor")); // Ann (editor, no email)

招牌特色:泛型與可辨識聯集

泛型讓同一個函式或型別能處理多種型別,同時保持型別之間的關聯。可辨識聯集(discriminated union)是由共用同一個字面值欄位的物件型別組成的聯集,因此對該欄位使用 switch 就能縮小型別,編譯器也能檢查是否處理了所有情況。

function first<T>(items: T[]): T | undefined {
  return items[0];
}
const n = first([10, 20]);   // n: number | undefined
const s = first(["a", "b"]); // s: string | undefined
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rect"; width: number; height: number };
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rect":
      return shape.width * shape.height;
  }
}
console.log(n, s, area({ kind: "rect", width: 2, height: 3 })); // 10 a 6

內建的工具型別(utility types)可由既有型別衍生出新型別:Partial<User> 讓所有屬性變成選用,Pick<User, "id" | "name"> 只保留部分屬性,Record<string, number> 則描述一個字典。

錯誤處理

錯誤的運作方式與 JavaScript 相同,使用 throw 和 try/catch。開啟 strict 選項後,捕捉到的值型別為 unknown,因為任何值都可以被拋出,所以使用前要先檢查。

function parseAge(text: string): number {
  const age = Number(text);
  if (!Number.isInteger(age) || age < 0) {
    throw new Error(`invalid age: ${text}`);
  }
  return age;
}
try {
  parseAge("abc");
} catch (err: unknown) {
  const message = err instanceof Error ? err.message : String(err);
  console.log(message); // invalid age: abc
}

完整的小程式:有型別的庫存管理

interface Item {
  name: string;
  price: number;
  qty: number;
}
type Summary = { total: number; cheapest?: Item; outOfStock: string[] };
function summarize(items: Item[]): Summary {
  let total = 0;
  let cheapest: Item | undefined;
  const outOfStock: string[] = [];
  for (const item of items) {
    total += item.price * item.qty;
    if (item.qty === 0) outOfStock.push(item.name);
    if (!cheapest || item.price < cheapest.price) cheapest = item;
  }
  return { total, cheapest, outOfStock };
}
const result = summarize([
  { name: "pen", price: 1.5, qty: 10 },
  { name: "book", price: 12, qty: 2 },
  { name: "bag", price: 30, qty: 0 },
]);
console.log(result.total, result.cheapest?.name, result.outOfStock);
// 39 pen [ 'bag' ]

試著把 qty: 10 改成 qty: "10":編譯器在程式執行之前就會回報錯誤。

初學者常見錯誤

  • 用 any 來消除錯誤。請優先使用 unknown 再縮小型別。
  • 過度使用 as 型別斷言,這等於叫編譯器相信你,而不是讓它檢查。
  • 在 tsconfig.json 中沒有開啟 strict。新專案請開啟它,它會啟用 null 檢查。
  • 寫出編譯器本來就能推論的型別,只會增加雜訊,並不會增加安全性。

接下來學什麼

閱讀官方的 TypeScript Handbook,並在 typescriptlang.org 的 TypeScript Playground 上實驗,它會並排顯示編譯後的 JavaScript 與錯誤訊息。接著學習 tsconfig.json 的各項選項、函式庫的型別宣告檔(@types/... 套件),以及搭配你慣用框架的 TypeScript 用法。

常見問題

TypeScript 是和 JavaScript 不同的語言嗎?

它是 JavaScript 的超集(superset):加入了型別語法與檢查器,再編譯成純 JavaScript。執行階段的行為與它產生的 JavaScript 完全相同。

應該先學 JavaScript 再學 TypeScript 嗎?

建議先學 JavaScript 基礎(變數、函式、物件、Promise),因為 TypeScript 並不會改變這些。之後再加入 TypeScript;熟悉執行階段的行為後,型別會更容易學。

interface 和 type 有什麼差別?

兩者都能描述物件結構。interface 可以被擴充,也能透過重複宣告來合併;type 別名還能為聯集、tuple 與其他非物件型別命名。單純描述物件結構時,兩者皆可。

TypeScript 會讓我的程式跑得更快嗎?

不會。型別在編譯時就被移除,所以輸出的程式執行速度與手寫的 JavaScript 相同。好處是更少的錯誤與更好的開發工具支援。

全部編碼教學