CHAPTER 04 / TYPE SAFETY

TypeScript:
給 JS 穿盔甲

JS 寫起來自由像穿短褲,但客戶網站上線後 bug 一堆也是因為這個自由。 TypeScript(簡稱 TS)就是給 JS 穿盔甲:型別檢查、自動補全、bug 在寫的時候就攔下來。 現在大公司新專案 90% 是 TS,會 TS 你的薪水多三成。

  • 看懂 React/Vue/Next 等任何 TS 專案
  • 會給變數、函式、物件加型別
  • 會用 interface、type、泛型寫可重用的型別
  • VS Code 自動補全暢通無阻
  • 能把舊 JS 專案逐步遷到 TS
LESSON 4.1

為什麼要學 TypeScript?

看這個 JS 函式:

function add(a, b) {
  return a + b;
}

add(1, 2);        // 3 ✓
add('1', 2);      // '12' 😱 字串拼接
add(undefined, 2);// NaN 💥 線上爆炸

JS 不會擋你。客戶用了,壞了,你才知道。TS 在寫的當下就告訴你錯:

function add(a: number, b: number): number {
  return a + b;
}

add('1', 2);  // ❌ 編輯器立刻紅線:'1' 不是 number

JS 像在地下道騎機車不戴安全帽,騎得很快很爽,跌倒就頭破血流。TS 是強制戴安全帽,剛開始覺得煩,跌倒時你會感謝它。

怎麼開始

# 全域安裝
npm install -g typescript

# 編譯
tsc app.ts        # 產生 app.js
tsc --watch       # 自動編譯

# 直接執行 .ts(推薦)
npm install -g tsx
tsx app.ts

實際工作中你不會手動 tsc,會用 Vite、Next.js 等框架幫你處理。

LESSON 4.2

基本型別

let name: string = '小明';
let age: number = 15;
let isStudent: boolean = true;
let hobbies: string[] = ['寫程式', '打球'];
let scores: number[] = [85, 92, 78];

// 元組(固定長度、固定型別)
let point: [number, number] = [10, 20];

// 任何型別(用了等於沒用 TS)
let wild: any = '隨便';

// 不知道是什麼,但要用之前要先檢查
let input: unknown = getUserInput();
if (typeof input === 'string') {
  input.toUpperCase();  // 這裡 TS 知道是 string
}

不要濫用 any。它讓 TS 完全失效。寫 any 不如寫 JS。實在不知道型別用 unknown,至少強迫你檢查。

聯合型別 |

let id: string | number;
id = 'abc';  // ✓
id = 123;    // ✓
id = true;   // ❌

// 字面量型別(只能是這幾個值)
let theme: 'light' | 'dark' | 'auto';
theme = 'red';  // ❌

null / undefined

let nick: string | null = null;
let phone?: string;  // 等同 string | undefined
LESSON 4.3

物件與 interface

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

const u: User = {
  id: 1,
  name: '小明',
  age: 15
};
u.id = 2;  // ❌ readonly

interface vs type

// interface(適合物件、可重複擴充)
interface User { name: string; }
interface User { age: number; }  // 自動合併

// type(適合聯合、複雜型別)
type ID = string | number;
type Status = 'pending' | 'done';
type Admin = User & { role: 'admin' };  // 交叉

規則:物件結構用 interface,其他用 type。React component 的 props 多數團隊用 type

函式型別

function greet(name: string): string {
  return `嗨 ${name}`;
}

// 箭頭函式
const add = (a: number, b: number): number => a + b;

// 函式型別別名
type Handler = (e: Event) => void;
const onClick: Handler = (e) => console.log(e);
LESSON 4.4

泛型 Generics

泛型 = 型別的變數。當你不想固定一種型別時用。

// 不用泛型
function firstString(arr: string[]): string { return arr[0]; }
function firstNumber(arr: number[]): number { return arr[0]; }
// 寫不完...

// 用泛型
function first<T>(arr: T[]): T {
  return arr[0];
}

first([1, 2, 3]);     // 自動推斷成 number
first(['a', 'b']);     // 自動推斷成 string

常見實例:API 回應

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

async function getUser(): Promise<ApiResponse<User>> {
  const r = await fetch('/api/me');
  return r.json();
}
LESSON 4.5

實用內建工具型別

interface User {
  id: number;
  name: string;
  email: string;
}

// 全部變選填
type PartialUser = Partial<User>;

// 全部變必填
type RequiredUser = Required<User>;

// 全部變 readonly
type FrozenUser = Readonly<User>;

// 只挑某些欄位
type UserPreview = Pick<User, 'id' | 'name'>;

// 排除某些欄位
type UserNoEmail = Omit<User, 'email'>;

// 從 union 排除
type Status = 'a' | 'b' | 'c';
type NoC = Exclude<Status, 'c'>;  // 'a' | 'b'
LESSON 4.6

tsconfig.json 必開選項

{
  "compilerOptions": {
    "target": "ES2022",
    "strict": true,           // 開!全部型別檢查
    "noUnusedLocals": true,
    "noImplicitAny": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "bundler"
  }
}

"strict": true 必開。新手常為了「先讓專案動」而關掉,最後 bug 跟 JS 一樣多,等於白裝 TS。

練習:型別化你的 To-Do List

把第 3 章的 To-Do List 從 JS 改成 TS:

  1. 定義 Todo interface(id、text、done、createdAt)
  2. 所有函式參數加型別
  3. localStorage 讀回來時做型別檢查
  4. 定義 Filter = 'all' | 'active' | 'done'
  5. 用泛型寫一個 useLocalStorage<T> 函式

常見卡關 FAQ

// TS 學員最常問的問題
VS Code 一直紅線怎麼辦?
紅線是 TS 在救你!讀錯誤訊息:通常是「型別 A 不能指派給 B」,看 A 跟 B 差在哪。多數情況加個 ? 或處理 null 就好。
第三方套件沒有型別?
先試 npm i -D @types/套件名。沒有的話自己寫個 declare module '套件名';types.d.ts,當 any 用。
interface 跟 type 我到底用哪個?
能用 interface 就用 interface(錯誤訊息較好讀、可宣告合併);要 union、tuple、複雜計算才用 type。團隊有規定就跟團隊。
as 強制轉型可以嗎?
能不用就不用。as 是「我比 TS 聰明」的承諾,承諾錯了上線就爆。比較常用的合理場景:document.querySelector('input') as HTMLInputElement
JS 專案要全改 TS 嗎?
不必。TS 支援 allowJs: true 漸進式遷移。先把新檔寫 TS,舊 JS 慢慢搬。一次全改往往會放棄。
← 上一章
03 JS