iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0
自我挑戰組

《轉職學習日記:JavaScript × Node.js × TypeScript × Docker × AWS ECS》系列 第 22

Day22 - 持續成長學習藍圖 - TypeScript(泛型與 Utility Types)

  • 分享至 

  • xImage
  •  

昨天把 Express 換成 TypeScript 版之後,今天繼續補型別功:泛型(Generics)Utility Types
這兩個主題讓型別更「可重用、可組合」,寫起來既靈活又安全。


1) 什麼是泛型(Generics)?

直覺比喻:把型別做成參數
就像函式的參數能決定行為,泛型參數能決定「這段程式操作的型別」。

最小實作:wrapValue<T>

function wrapValue<T>(value: T): { value: T } {
  return { value };
}

// 型別推論會自動帶入
const a = wrapValue(123);          // { value: number }
const b = wrapValue("hello");      // { value: string }
const c = wrapValue({ id: 1 });    // { value: { id: number } }

加上限制(extends)

有時候不想讓 T 隨便來,可以「約束」它:

// 只能接受「有 id 屬性」的物件
function withId<T extends { id: number }>(obj: T) {
  return obj.id;
}

withId({ id: 1, name: "TS" }); // ✅ 1
// withId({ name: "no id" });  // ❌ 型別錯誤

泛型在陣列/回傳值的常見用法

function first<T>(list: T[]): T | undefined {
  return list[0];
}

const n = first([1, 2, 3]);        // n: number | undefined
const s = first(["a", "b"]);       // s: string | undefined

2) Utility Types:把型別當積木重組

先定義一個 Todo 型別,後面用它示範。

type Todo = {
  id: number;
  task: string;
  done: boolean;
  note?: string;
};

(a) Partial<T>:全部變成可選

適合「更新」型的輸入(只改部分欄位)。

type TodoUpdate = Partial<Todo>;
/*
等同:
{
  id?: number;
  task?: string;
  done?: boolean;
  note?: string;
}
*/

function updateTodo(target: Todo, patch: TodoUpdate): Todo {
  return { ...target, ...patch };
}

(b) Pick<T, K>:挑出想要的欄位

適合 DTO 或 API 回應只列出必要欄位。

// 建立 Todo 的「建立輸入」型別,只需要 task/note
type CreateTodoDto = Pick<Todo, "task" | "note">;

const input: CreateTodoDto = { task: "寫 Day 22", note: "重點:泛型" };
// id/done 不在這個型別裡,所以不能亂塞

(c) Omit<T, K>:排除不要的欄位

適合隱藏內部欄位(例如 id 是後端產的)。

// 用於「建立」時,前端不提供 id/done
type CreateTodoPayload = Omit<Todo, "id" | "done">;
// { task: string; note?: string }

(d) Readonly<T>:變成唯讀

防止誤改資料(例如回傳值)。

function getTodo(): Readonly<Todo> {
  return { id: 1, task: "學 TS", done: false };
}

const t = getTodo();
// t.done = true; // ❌ Readonly 禁止修改

3) 小整合:把 Utility Types 用在 API 型別流程

建立 → 回傳

type CreateTodoDto = Pick<Todo, "task" | "note">;
type TodoResponse = Readonly<Todo>;

function createTodo(dto: CreateTodoDto): TodoResponse {
  const newTodo: Todo = {
    id: Date.now(),
    task: dto.task,
    done: false,
    note: dto.note,
  };
  return newTodo; // 自動套到 Readonly<Todo>
}

更新 → 部分欄位

type UpdateTodoDto = Partial<Pick<Todo, "task" | "done" | "note">>;

function patchTodo(target: Todo, dto: UpdateTodoDto): Todo {
  return { ...target, ...dto };
}

小心得:PickPartial 很常見,因為「更新」通常只允許特定欄位,而且是選填。


4) 額外技巧:泛型 + 預設型別(想知道可以看)

在比較大的工具函式中,會用到 預設泛型

// 預設把錯誤型別當作 string
type Result<TData, TError = string> = {
  ok: boolean;
  data?: TData;
  error?: TError;
};

function ok<T>(data: T): Result<T> {
  return { ok: true, data };
}
function fail<E = string>(error: E): Result<never, E> {
  return { ok: false, error };
}

const r1 = ok({ id: 1 });           // Result<{id: number}, string>
const r2 = fail("Oops");            // Result<never, string>
const r3 = fail<{ code: number }>({ code: 500 }); // 自訂錯誤型別

5) 今日實作回顧

✅ 寫一個泛型函式 wrapValue<T>(value: T): { value: T }

(上面已完成)

✅ 用 Pick / Omit 簡化 Todo 型別

  • CreateTodoDto = Pick<Todo, "task" | "note">
  • CreateTodoPayload = Omit<Todo, "id" | "done">
  • UpdateTodoDto = Partial<Pick<Todo, "task" | "done" | "note">>

🎯 學習心得 / 今日收穫

  • 泛型 讓「可重用的邏輯」也能保有型別安全,像把「型別」變成參數一樣彈性。
  • Utility Types 就像型別積木:PartialPickOmitReadonly 組一組,就能很快產出 DTO、更新輸入、唯讀輸出。
  • 實際用在 Todo 的建立/更新/回傳流程,真的能把型別規格講清楚,IDE 自動提示也超好用。

上一篇
Day21 - 持續成長學習藍圖 - TypeScript(Express)
下一篇
Day23 - 持續成長學習藍圖 - TypeScript(DTO 與 class-validator)
系列文
《轉職學習日記:JavaScript × Node.js × TypeScript × Docker × AWS ECS》24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言