【TypeScript 入門教學】#05 泛型入門:讓型別也能當參數

測驗:TypeScript 泛型入門

共 5 題,點選答案後會立即顯示結果

1. 泛型(Generics)最核心的概念是什麼?

  • A. 讓函式可以接收多個參數
  • B. 讓型別也能當參數傳遞,寫一次適用所有型別
  • C. 讓 TypeScript 自動幫你寫程式碼
  • D. 讓 JavaScript 支援強型別檢查

2. 在泛型命名慣例中,<K, V> 通常代表什麼?

  • A. Kind(種類)和 Variable(變數)
  • B. 兩個通用型別參數,沒有特殊含義
  • C. Key(鍵)和 Value(值),用於處理鍵值對
  • D. Keyword(關鍵字)和 Validator(驗證器)

3. 以下程式碼中,Promise<User> 代表什麼意思?

const result: Promise<User> = fetchUser();
  • A. 一個 User 型別的同步回傳值
  • B. 一個未來會拿到 User 的非同步結果
  • C. 一個可能是 User 也可能是 null 的值
  • D. 一個包含多個 User 的陣列

4. 以下泛型函式的作用是什麼?

function swap<T, U>(a: T, b: U): [U, T] { return [b, a]; }
  • A. 將兩個相同型別的值合併成陣列
  • B. 複製兩個值並回傳副本
  • C. 將兩個值轉換為同一型別後回傳
  • D. 接收兩個不同型別的值,交換順序後回傳

5. 以下程式碼中,呼叫 getLength(123) 會發生什麼事?

function getLength<T extends { length: number }>(arg: T): number { return arg.length; }
  • A. 回傳 3,因為 123 有三位數
  • B. 回傳 undefined,因為 number 沒有 length
  • C. 編譯錯誤,因為 number 型別沒有 length 屬性,不符合泛型約束
  • D. 執行時拋出例外錯誤

一句話說明

泛型就是「型別的參數」,讓同一段程式碼能對應不同的型別。

為什麼需要泛型?

假設 AI 幫你寫了一個「把東西包進陣列」的函式:

function wrapInArray(value: string): string[] {
    return [value];
}

這只能包字串。如果你也想包數字呢?難道要再寫一份?

function wrapNumberInArray(value: number): number[] {
    return [value];
}

這太蠢了。泛型就是解決這個問題的:寫一次,適用所有型別

function wrapInArray<T>(value: T): T[] {
    return [value];
}
Code language: HTML, XML (xml)

翻譯:「這個函式接收一個某種型別 T 的值,回傳一個 T 陣列。至於 T 是什麼,用的時候再決定。」


最小範例

function identity<T>(arg: T): T {
    return arg;
}
Code language: JavaScript (javascript)

這是最經典的泛型範例。逐行翻譯:

function identity<T>(arg: T): T {
//                ↑      ↑    ↑
//         宣告型別參數 T  |    回傳型別也是 T
//                   參數型別是 T
    return arg;
}
Code language: JavaScript (javascript)

白話翻譯:「這個函式接收什麼型別,就回傳什麼型別。」

使用時,TypeScript 會自動推斷 T 是什麼:

const a = identity("hello");   // T 被推斷為 string,a 是 string
const b = identity(42);        // T 被推斷為 number,b 是 number
Code language: JavaScript (javascript)

你也可以手動指定:

const a = identity<string>("hello");   // 明確指定 T 是 string
Code language: JavaScript (javascript)

逐行翻譯:解讀 <T> 語法

在 AI 產生的程式碼裡,你會大量看到角括號 <> 搭配大寫字母。以下是翻譯指南:

function getFirst<T>(list: T[]): T {
//               ↑        ↑      ↑
//          「我要用到一種型別,暫時叫它 T」
//                    「參數是 T 組成的陣列」
//                           「回傳一個 T」
    return list[0];
}
Code language: PHP (php)

你可以把 <T> 想成函式的「型別參數」,就像 (arg: T) 是值的參數一樣。

常見的泛型命名慣例

名稱 代表的意思 常出現在
T Type(型別) 通用場景
K Key(鍵) 物件的 key
V Value(值) 物件的 value
U 第二個型別 需要兩個型別參數時
E Element(元素) 集合、陣列

看到 <K, V> 就知道是在處理「鍵值對」,看到 <T, U> 就是在處理「兩種型別」。


常見變化

AI 產生的程式碼中,泛型最常出現在以下幾種情境。

變化 1:內建泛型型別

這些你一定會遇到,必須認得:

const names: Array<string> = ["Alice", "Bob"];
// 翻譯:「一個裝 string 的陣列」
// 等同於 string[]

const response: Promise<Response> = fetch("/api/users");
// 翻譯:「一個未來會拿到 Response 的 Promise」

const settings: Record<string, number> = { width: 100, height: 200 };
// 翻譯:「key 是 string、value 是 number 的物件」
Code language: JavaScript (javascript)

速查表

你會看到 意思
Array<string> 字串陣列,等同 string[]
Promise<User> 未來會拿到 User 的非同步結果
Record<string, number> 一個 key 是字串、value 是數字的物件
Map<string, User> 一個字串對應 User 的 Map
Set<number> 一組不重複的數字
Partial<User> User 的所有欄位都變成可選
Pick<User, "name"> 只取 User 的 name 欄位
Omit<User, "password"> User 去掉 password 欄位

變化 2:泛型箭頭函式

AI 常用箭頭函式寫泛型:

const getFirst = <T>(list: T[]): T => list[0];
// 翻譯:「取得陣列的第一個元素,型別跟陣列元素一樣」
Code language: PHP (php)

注意:在 .tsx 檔案裡(React),你可能看到這種寫法,多了一個逗號:

const getFirst = <T,>(list: T[]): T => list[0];
//                 ↑ 這個逗號是為了避免 JSX 解析器把 <T> 當成 HTML 標籤
Code language: PHP (php)

變化 3:多個型別參數

function swap<T, U>(a: T, b: U): [U, T] {
    return [b, a];
}
// 翻譯:「接收兩個不同型別的值,交換順序回傳」

const result = swap("hello", 42);   // result 的型別是 [number, string]
Code language: HTML, XML (xml)

變化 4:泛型 interface 和 type

AI 最常用泛型來定義 API 回應格式:

// 定義通用的 API 回應結構
interface ApiResponse<T> {
    data: T;            // 實際資料,型別由 T 決定
    status: number;     // HTTP 狀態碼
    message: string;    // 訊息
}

// 使用時指定 T 是什麼
type UserResponse = ApiResponse<User>;
// 翻譯:「一個 API 回應,裡面的 data 是 User 型別」

type ProductListResponse = ApiResponse<Product[]>;
// 翻譯:「一個 API 回應,裡面的 data 是 Product 陣列」
Code language: PHP (php)

白話翻譯:「ApiResponse 是一個回應的模板,T 是可以替換的部分。」

type 也可以做一樣的事:

type Result<T> = {
    success: boolean;
    data: T | null;
    error: string | null;
};
Code language: JavaScript (javascript)

泛型約束:extends 的用法

有時候泛型不能是「任何型別」,需要限制。這時用 extends

function getLength<T extends { length: number }>(arg: T): number {
    return arg.length;
}
// 翻譯:「T 必須有 length 屬性,這個函式回傳它的長度」
Code language: JavaScript (javascript)

使用效果:

getLength("hello");      // OK,string 有 length
getLength([1, 2, 3]);    // OK,陣列有 length
getLength(123);          // 錯誤!number 沒有 length
Code language: JavaScript (javascript)

常見的約束寫法

// 約束 T 必須是某個 interface 的子集
function printName<T extends { name: string }>(obj: T) {
    console.log(obj.name);
}
// 翻譯:「接收任何有 name 屬性的物件」

// 約束 K 必須是 T 的某個 key
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}
// 翻譯:「從物件取出指定 key 的值,key 必須是物件真的有的 key」
Code language: HTML, XML (xml)

keyof T 的意思是「T 的所有 key 的集合」。所以 K extends keyof T 就是「K 必須是 T 的某個 key」。

const user = { name: "Alice", age: 25 };
getProperty(user, "name");    // OK,回傳 string
getProperty(user, "age");     // OK,回傳 number
getProperty(user, "email");   // 錯誤!user 沒有 email
Code language: JavaScript (javascript)

實際場景:AI 常怎麼用泛型

場景 1:通用的 API 請求函式

async function fetchApi<T>(url: string): Promise<T> {
    const response = await fetch(url);
    const data: T = await response.json();
    return data;
}

// 使用時指定回傳型別
const user = await fetchApi<User>("/api/user/1");
const products = await fetchApi<Product[]>("/api/products");
Code language: JavaScript (javascript)

翻譯:「fetchApi 是一個通用的 API 呼叫函式。用的時候告訴它回傳什麼型別,它就會幫你做型別檢查。」

場景 2:React 的 useState

如果你有碰到 React,這是最常見的泛型用法:

const [user, setUser] = useState<User | null>(null);
// 翻譯:「建立一個 state,值可以是 User 或 null,初始值是 null」
Code language: JavaScript (javascript)

場景 3:通用的工具型別

type Nullable<T> = T | null;
// 翻譯:「Nullable<User> 就是 User | null」

type AsyncFunction<T> = () => Promise<T>;
// 翻譯:「一個回傳 Promise<T> 的函式」
Code language: JavaScript (javascript)

必看懂 vs 知道就好

必看懂(會一直出現)

  • <T> 在函式或型別後面:宣告泛型參數
  • Array<string>Promise<User>:內建泛型型別
  • ApiResponse<T> 這類自訂泛型 interface
  • <T extends SomeType>:泛型約束

知道就好(遇到再查)

  • 條件型別:T extends U ? X : Y,型別版的三元運算
  • infer 關鍵字:T extends Promise<infer U> ? U : T,從型別裡提取子型別
  • 映射型別:{ [K in keyof T]: ... },批量轉換型別
  • 模板字面量型別:` ${T}Changed ,用字串模板生成型別 這些進階用法在函式庫的型別定義裡會出現,但日常開發中不需要自己寫。遇到時知道「這是在做型別運算」就夠了。 --- ## Vibe Coder 檢查點 看到泛型程式碼時確認: - [ ] 裡的 T 代表什麼?能猜出來嗎?(看命名和上下文) - [ ] 使用泛型函式時,T 是怎麼被決定的?(自動推斷還是手動指定) - [ ] 如果有 extends 約束,T 被限制成什麼範圍? - [ ] AI 定義的泛型 interface(像 ApiResponse<T>),T 在實際使用時被替換成了什麼? --- ## 總結 泛型的核心概念只有一個:**型別也可以當參數傳**。 | 概念 | 程式碼 | 白話翻譯 | |------|--------|---------| | 泛型函式 | function fn(arg: T): T | 進什麼型別,出什麼型別 | | 泛型 interface | interface Box { value: T } | 一個模板,T 可以替換 | | 內建泛型 | Array | 裝字串的陣列 | | 泛型約束 | | T 必須有 name 屬性 | | 多型別參數 | | 兩個型別參數,通常是 key 和 value | 下次看到 AI 寫出 ` 時,別慌。把 T 想成「型別的參數」,就像函式的參數一樣,用的時候再填入實際的型別就好了。

進階測驗:TypeScript 泛型入門

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在開發一個後端 API,需要為不同的資源(User、Product、Order)設計統一的回應格式,都包含 datastatusmessage 三個欄位,但 data 的型別會隨資源不同而改變。最佳的做法是什麼? 情境題

  • A. 為每種資源各寫一個 interface:UserResponseProductResponseOrderResponse
  • B. 將 data 的型別設為 any,這樣什麼資料都能放
  • C. 定義泛型 interface ApiResponse<T>,使用時指定 T 為不同的資源型別
  • D. 用 union type User | Product | Order 作為 data 的型別

2. 你需要寫一個工具函式,從任意物件中安全地取出某個屬性的值。你希望 TypeScript 在編譯期就能檢查 key 是否合法,而且回傳值的型別要精確。以下哪種寫法最合適? 情境題

  • A. function getProp(obj: any, key: string): any
  • B. function getProp<T, K extends keyof T>(obj: T, key: K): T[K]
  • C. function getProp<T>(obj: T, key: string): T
  • D. function getProp<T>(obj: Record<string, T>, key: string): T

3. 你在 React 專案的 .tsx 檔案中寫了一個泛型箭頭函式,但編輯器報出語法錯誤。以下是你的程式碼:情境題

// MyComponent.tsx const getFirst = <T>(list: T[]): T => list[0];

最可能的原因和解決方式是什麼?

  • A. 箭頭函式不支援泛型,必須改用 function 宣告
  • B. T 需要先用 type T = unknown 宣告才能使用
  • C. .tsx 檔案不支援泛型語法,需要改用 .ts 副檔名
  • D. JSX 解析器把 <T> 當成 HTML 標籤了,在 T 後面加逗號寫成 <T,> 即可解決

4. 以下程式碼在編譯時報錯,問題出在哪裡? 錯誤診斷

function logLength<T>(arg: T): void { console.log(arg.length); // Error: Property ‘length’ does not exist on type ‘T’ } logLength(“hello”); logLength([1, 2, 3]);
  • A. T 不能用在 console.log 裡面
  • B. 呼叫時沒有手動指定 <string><number[]>,導致型別推斷失敗
  • C. 泛型 T 沒有加上約束,TypeScript 無法確定 T 一定有 length 屬性,需改為 T extends { length: number }
  • D. void 回傳型別和 console.log 衝突,應該改為回傳 number

5. 以下程式碼的型別定義有什麼問題? 錯誤診斷

interface ApiResponse { data: any; status: number; message: string; } const userRes: ApiResponse = { data: { name: “Alice” }, status: 200, message: “OK” }; // 之後使用 userRes.data 時沒有任何型別提示 console.log(userRes.data.name); // 能執行,但 TypeScript 不會檢查 console.log(userRes.data.email); // 也能執行,但 email 根本不存在!
  • A. interface 不能包含 any 型別,應該用 unknown 代替
  • B. data 用了 any 失去型別檢查,應該改用泛型 ApiResponse<T>,讓 data: T 在使用時指定具體型別
  • C. status 應該用 string 而不是 number,這導致型別推斷失效
  • D. 需要在 console.log 之前用 as User 做型別斷言才能取得型別檢查

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *