Javascript深拷貝淺拷貝作法

在 JavaScript 中,淺拷貝(shallow copy)深拷貝(deep copy) 是物件或陣列複製時很重要的概念。以下是詳細的解說,並搭配常見題目與解答,幫助你徹底理解這兩者的差異。


🔍 1. 淺拷貝 vs 深拷貝 差異解說

✅ 淺拷貝(Shallow Copy)

淺拷貝:只複製第一層的屬性,如果屬性是物件或陣列的話,拷貝的是參考(reference),不是值本身。

常見方法:

  • Object.assign()
  • 展開運算子 { ...obj }[ ...arr ]

範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj1 = {
name: 'Tom',
info: {
age: 25
}
};

const shallowCopy = { ...obj1 };

shallowCopy.name = 'Jerry'; // ✅ 不會影響原物件
shallowCopy.info.age = 30; // ⚠️ 原物件也會被改變!

console.log(obj1.info.age); // 30(被改到)

✅ 深拷貝(Deep Copy)

深拷貝:不僅拷貝第一層,還會遞迴地複製所有子屬性,即便是物件或陣列,也會整個拷貝成新的。

常見方法:

  • JSON.parse(JSON.stringify(obj))(⚠️ 有限制,不能複製 functionundefinedSymbol 等)
  • 使用 遞迴函式手動拷貝
  • 使用第三方函式庫(如 lodash_.cloneDeep()

範例:

1
2
3
4
5
6
7
8
9
10
11
12
const obj1 = {
name: 'Tom',
info: {
age: 25
}
};

const deepCopy = JSON.parse(JSON.stringify(obj1));

deepCopy.info.age = 30;

console.log(obj1.info.age); // ✅ 仍為 25(原物件沒變)

🧠 2. 常見面試題與解答

📌 題目 1:請解釋什麼是深拷貝與淺拷貝,並舉例說明差異?

解答:

淺拷貝是複製物件的第一層屬性,但如果屬性是物件,會複製的是參考;深拷貝則會遞迴複製所有屬性,完全拷貝一份新的物件。

範例:

1
2
3
4
5
6
7
8
9
const obj = { a: 1, b: { c: 2 } };
const shallow = { ...obj };
const deep = JSON.parse(JSON.stringify(obj));

shallow.b.c = 99;
console.log(obj.b.c); // 99,說明是淺拷貝

deep.b.c = 100;
console.log(obj.b.c); // 99,不變,說明是深拷貝

📌 題目 2:寫一個遞迴的 deepClone 函數

解答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;

if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}

const result = {};
for (const key in obj) {
result[key] = deepClone(obj[key]);
}

return result;
}

// 測試
const obj1 = { a: 1, b: { c: 2 } };
const clone = deepClone(obj1);
clone.b.c = 99;

console.log(obj1.b.c); // 2(沒被改變)

📌 題目 3:以下程式碼輸出是什麼?為什麼?

1
2
3
4
const original = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, original);
copy.b.c = 99;
console.log(original.b.c);

答案:

1
99

原因: Object.assign 是淺拷貝,b 屬性是物件,所以複製的是 reference。修改 copy.b.c 等同於改了 original.b.c


🧰 延伸:使用 lodash 的 cloneDeep

1
2
3
4
5
6
7
import _ from 'lodash';

const obj = { a: 1, b: { c: 2 } };
const deepCopy = _.cloneDeep(obj);
deepCopy.b.c = 99;

console.log(obj.b.c); // 2

🔚 總結對比表格

特性 淺拷貝 (Shallow Copy) 深拷貝 (Deep Copy)
複製層級 僅第一層 所有層級
參考關係 內層物件是參考 完全複製
方法 Object.assign... 遞迴函數、cloneDeep
安全性 修改內層會影響原物件 修改不會影響原物件

深淺拷貝最佳實踐

目的 / 場景 建議做法 使用方式 原因
只複製一層簡單資料(如物件無巢狀結構) ✅ 淺拷貝 Object.assign({}, obj){ ...obj } 效能高、語法簡潔,適用於扁平資料
複製陣列的第一層元素(無巢狀) ✅ 淺拷貝 [...arr]arr.slice() 常見陣列操作,快速又直觀
資料包含巢狀物件或多層結構,需要完全脫鉤原資料 ✅ 深拷貝 JSON.parse(JSON.stringify(obj))(限純資料)
或自寫 deepClone() 遞迴函數
或使用 lodash.cloneDeep()
淺拷貝無法處理巢狀結構,會有參考共享風險
資料包含函式、undefined、Symbol 等特殊值 ✅ 使用 lodash.cloneDeep() 或自訂 deepClone _.cloneDeep(obj) 或自定義深拷貝函式支援各型別 JSON.parse 方式會遺失特殊型別,導致資料不完整
效能敏感場景下(大型資料結構)不需完全複製 ✅ 淺拷貝 + 不變資料原則 只做淺拷貝後避免修改巢層資料,例如 { ...obj } 然後不改動 obj.b 等巢層 深拷貝成本高,若可避免修改參考資料則淺拷貝更有效率
需避免資料共享或引用污染(多人共用資料來源) ✅ 深拷貝 使用自寫 deepClone()lodash.cloneDeep() 資料共享會導致非預期變動,深拷貝可徹底分離資料

Javascript傳參考、傳值差異

在 JavaScript 中,「Call by Value(傳值)」與「Call by Reference(傳參考)」是常被提起但容易誤解的觀念,特別是在處理物件與陣列時。


✅ JS 中的參數傳遞:Call by Value 還是 Call by Reference?

🔑 關鍵理解:

👉 JavaScript 中的所有參數傳遞都是「傳值」(Call by Value)!
但當你傳的是參考型別的變數(如物件、陣列),你傳的是「那個記憶體位址的值」,所以看起來像「傳參考」。


🧠 1. Call by Value(傳值)

  • 適用於:原始型別(Primitive types)
  • 傳入函式時,複製的是值本身
  • 在函式內改變變數,不會影響外部變數
1
2
3
4
5
6
7
8
function changeValue(x) {
x = x + 10;
console.log("inside:", x); // 20
}

let a = 10;
changeValue(a);
console.log("outside:", a); // 10 ✅ 不變

🧠 2. Call by Reference(表面上是傳參考)

  • 適用於:參考型別(Object, Array, Function 等)
  • 傳入的是該物件的記憶體參考值(位址)
  • 在函式內修改物件內容會影響外部原始物件
1
2
3
4
5
6
7
function modify(obj) {
obj.name = "Bob"; // 修改原物件內容
}

let person = { name: "Alice" };
modify(person);
console.log(person.name); // Bob ✅ 被改變了

❗ 但是注意:重新賦值無法改變原物件

1
2
3
4
5
6
7
function change(obj) {
obj = { name: "Charlie" }; // 只是換了一個新物件
}

let user = { name: "Alice" };
change(user);
console.log(user.name); // Alice ❗ 沒被改變

🔍 為什麼?
因為 obj = {...} 是在函式內部重新指定一個新的記憶體參考,不會影響原來的參考地址本身


✅ 總結比較表格

類型 傳遞方式 是否可改變原值 說明
原始型別(數字、字串等) Call by Value ❌ 否 傳入函式時複製值,函式內更改不影響外部
參考型別(物件、陣列等) Call by Value(但是傳 reference 的值) ✅ 可以改內容
❌ 不能重新指定
傳的是記憶體位置的值,可改內容,但不能改記憶體位置本身

🧪 題目挑戰(幫助理解)

❓這段程式碼會輸出什麼?

1
2
3
4
5
6
7
8
function update(arr) {
arr.push(4);
arr = [1, 2]; // 這一行有效嗎?
}

const list = [0];
update(list);
console.log(list); // ??

答案

1
[0, 4] ✅

因為 arr.push(4) 是修改原陣列內容(有效)
arr = [1, 2] 是重新指定變數,不會影響外部的 list


📌 總結一句話:

JavaScript 是 Call by Value,但當你傳的是物件或陣列時,你是傳了參考的值,所以函式能改內容但不能換物件本身


最佳實踐

使用情境描述 是否建議拷貝 推薦方式 原因 / 說明
僅讀取物件資料,不會修改 ❌ 不需要 直接傳入 沒有副作用,效能較佳
函式內部會修改第一層資料(但不應影響原始資料) ✅ 建議 淺拷貝{ ...obj } 防止污染原始資料(僅拷貝第一層)
函式內會修改巢狀屬性,如 obj.user.age = 30 ✅ 必須 深拷貝cloneDeep(obj) 避免巢狀結構共用參考,防止原始物件被改變
僅需要物件中部分資料(如一個 key) ✅ 建議 解構取值function({ id }) 只傳入需要的屬性,可避免整包物件傳入、避免意外修改
處理複雜資料、可能有循環結構或特殊型別(如 function) ✅ 建議 lodash.cloneDeep() 或自定函式 避免 JSON.stringify 限制,保留完整資料類型
效能敏感、不希望多餘拷貝,但又需保護資料 ✅ 可選 防變更操作 + 明確文件註記 可避免不必要的拷貝成本,但需確保開發者不誤改資料
需要修改參數,但只希望在函式內有效(不污染外部) ✅ 必須 深拷貝 + 修改副本 保持資料純淨,函式有單一責任,易於除錯與維護

補充

原則一:不確定資料是否會被改,寧可先拷貝

原則二:若資料大且拷貝成本高,須權衡性能與安全

原則三:資料敏感(如設定檔、使用者狀態)務必拷貝

Javascript的參考型別

在 JavaScript 中,資料型別(Data Types) 可以分為兩大類:


✅ 1. 值型別(Primitive Types)

  • 也稱「原始型別」,儲存的是值本身
  • 資料在記憶體中直接存儲值
  • 複製時是複製值(值拷貝)
型別 範例
string 'hello'
number 42
boolean true / false
undefined undefined
null null
symbol Symbol()
bigint 10n

✅ 2. 參考型別(Reference Types)

  • 儲存的是資料的「記憶體位址(reference)」
  • 資料實體存在記憶體堆(heap)中
  • 複製的是記憶體位址(引用拷貝)
  • 多個變數可指向同一份資料,修改一處其他處也會變

📦 常見的參考型別:

型別 範例
Object { name: 'Tom' }
Array [1, 2, 3]
Function function() {}
Date new Date()
RegExp /abc/
Map new Map()
Set new Set()
其他物件型別 WeakMapWeakSet

🔍 差異範例說明

✳️ 值型別是「值拷貝」

1
2
3
4
5
let a = 10;
let b = a;

b = 20;
console.log(a); // 10 ✅ a 不變

✳️ 參考型別是「位址拷貝」

1
2
3
4
5
let obj1 = { name: 'Alice' };
let obj2 = obj1;

obj2.name = 'Bob';
console.log(obj1.name); // Bob ❗原物件也被改了

❗為什麼這很重要?

因為它會影響你:

  • 物件/陣列是否共享資料
  • 如何寫拷貝(深拷貝 vs 淺拷貝)
  • 函式參數是否會改變外部資料(因為是傳參考)

✅ 判斷資料型別方法

1. typeof

1
2
3
4
5
6
typeof 123        // 'number'
typeof 'hi' // 'string'
typeof {} // 'object'
typeof [] // 'object' ❗(陣列也是物件)
typeof null // 'object' ❗(語言設計缺陷)
typeof function(){} // 'function'

2. Array.isArray()

1
2
Array.isArray([]); // true
Array.isArray({}); // false

3. instanceof

1
2
3
[] instanceof Array       // true
{} instanceof Object // true
new Date() instanceof Date // true

🧠 小結:你該記住的是…

類型 是否為參考型別 複製的是什麼? 修改後是否影響原資料
number ❌ 否 值(Value)
string ❌ 否 值(Value)
object ✅ 是 記憶體位置(Reference) ✅ 會影響原資料
array ✅ 是 記憶體位置 ✅ 會影響原資料

[Day07] Typescript什麼時候用interface

這回想藉此來探討實際應用時何時該用interface?
在過往實務應用時會遇到若每個處理的物件都建interface那一個頁面就動輒1~5個以上。
而算上巢狀物件,就更多了,所以這回利用AI建議來探討一下。

最後,則會以我最近寫個小專案「露營日」作為範例,讓AI幫我判讀哪些其實可以省、哪些必要。

Q1:詢問我在寫typescript專案,感覺一個頁面每個物件都建立interface太多了,到底哪些才要哪些用any?

解答:

這個問題超多人在 TypeScript 專案會遇到 👍
你現在的困擾其實就是 型別要不要「嚴格」定義 vs 偷懶用 any 的取捨。

我幫你拆成幾個層次來看:


🚫 不建議直接用 any 的地方

  1. API 回傳的資料

    • API 是最常變動的地方,如果用 any,資料格式一變你就會踩雷。
    • 建立 interface/type 可以立刻知道少了欄位或欄位型態錯誤。
      ✅ 這裡建議要有 interface。
  2. 跨檔案、多人協作的資料結構

    • 例如 User、Product、Order 這種「全專案通用」的物件。
    • 沒型別大家就會各自定義,結果最後爆掉。
      ✅ 要建立 interface 或 type,通常會集中放在 types/ 資料夾。
  3. 複雜邏輯需要推導型別

    • 假設一個 function 處理一堆欄位,如果沒型別你會不知道 function 需要什麼資料。
      ✅ 建議用 interface 來限制 input/output。

🟡 可以「偷懶」不用 interface 的地方

  1. 一次性的小物件

    1
    const options = { theme: 'dark', showSidebar: true }
    • 這種本地小物件,TS 自己會推斷型別,不一定要手動寫 interface。
  2. 快速原型 / Demo

    • 你只是在測試功能,或這頁很快會被刪掉,不需要寫到超嚴格。
    • 可以用 anyunknown,之後再慢慢補齊。
  3. 第三方套件輸出的資料很雜

    • 有些套件沒提供型別,資料又一堆巢狀,硬定義超麻煩。
    • 可以先用 any,等真的需要用到再逐步補型別。

🎯 實務上常見做法

  • Domain Models(使用者、商品、訂單):一定寫 interface/type。
  • API Response:通常用 type ApiResponse<T> = { data: T; error?: string } 這種 pattern。
  • 小型狀態 / component props:TS 自己會推斷就不用寫。
  • 還不確定結構的資料:先用 anyunknown,等穩定後再補 interface。

👉 簡單原則就是:

  • 跨頁面/多人共用 → 要 interface
  • 一次性/很簡單 → 交給 TS 推斷就好
  • 實在太麻煩 → 先用 any/unknown,之後再補

Q2.那像是angular 或vue3或react這種 在html上顯示的資料該訂嗎?

這個其實就是 前端框架 (Angular / Vue3 / React)模板 (HTML/JSX) 顯示資料時,要不要訂型別?


📌 分情境來看

1. API 資料要渲染到畫面上

1
2
3
4
5
6
7
8
9
10
// 假設從 API 拿到 user
interface User {
id: number
name: string
email?: string
}

users: User[] = [] // Angular
const users = ref<User[]>([]) // Vue3
const [users, setUsers] = useState<User[]>([]) // React
  • 要型別
    因為這是 **外部資料來源 (API)**,一旦 API 改了,畫面上就可能噴錯。
    有 interface → TS 在你 {{ user.name }}user.name 時會自動提示、也能避免 typo。

2. 本地暫存的小資料 / UI 狀態

1
2
isLoading = true
selectedTab = 'home'
  • 不用特別寫 interface
    這種 TS 會自動推斷,寫 interface 反而太重。
    (除非是 大型複雜狀態,才會包一個 interface UiState)

3. 表單資料

1
2
3
4
5
6
7
8
9
10
11
interface CampForm {
date: string
people: number
note?: string
}

campForm = this.fb.group<CampForm>({
date: [''],
people: [0],
note: [''],
})
  • 要型別
    因為表單資料要送 API / 驗證,沒有型別很容易傳錯。

4. Component Props

1
2
3
4
5
6
7
8
// Vue3
defineProps<{ title: string; count?: number }>()

// React
type ButtonProps = {
label: string
onClick?: () => void
}
  • 要型別
    因為 props 是 元件的 API,型別就是保護你和使用者不傳錯。

🎯 實務上的建議

  • API / Props / 表單一定要 interface/type
  • 畫面上的小資料 (狀態、flag)交給 TS 自動推斷就好
  • 如果型別很麻煩但資料要顯示 → 先 any → 之後慢慢抽 interface

👉 一句話原則:
「會跨元件或送 API 的資料 → 訂型別」
「只在本地 UI 用的小資料 → TS 自己推斷就好」

Q3.我讓AI幫我判斷了我這還沒整理的Angular程式碼,哪些該省哪些該加。

.ts程式碼
https://github.com/yuhsiang237/CampDay/blob/80af24eb86eb064a22ac7acf649b3b104600f46f/src/app/result/result.ts

我幫你把這份 Angular v20 程式碼掃描過一遍,從「哪些要 interface」「哪些可以偷懶」的角度整理 👍


✅ 建議一定要有 interface/type

這些是「跨頁面 or API 資料」,型別能大幅降低錯誤:

  • CampSite

    • 你已經寫好,對應 CSV → 當然要保留。
  • CampSearch

    • 來自表單、要跟 API 溝通 → 一定要型別。
  • CampDistData

    • 你已經用來做 groupBy 的結果,畫面會顯示 district / data → 保留。
  • WeatherAPI

    • 你有 cityApiMap: WeatherAPI = weatherAPI as WeatherAPI; → 建議保留。

👉 這些 interface 應該集中放在 interfaces/ 資料夾,做「共用 domain 型別」。


🟡 建議可以再補充型別(目前用 any 的地方)

現在很多 any,會讓你失去 TS 提示:

  • formData: any;

    • 建議 → 用 Partial<CampSearch>Record<string, string>
  • weather: any;

    • 建議 → 抽成 interface WeatherResponse,至少定義 records.Locations[0].Location 結構,不然 HTML 綁定會沒提示。
  • locationWeather: any[] = [];

    • 建議 → 改成 WeatherLocation[](要不要定義完整,看你需求,可以先定義部分欄位)。
  • normalizeWeatherSlots(raw: any): any

    • 建議 → 回傳型別 Record<string, (WeatherSlot|null)[]>
      至少定義 WeatherSlot { timeRange: string; label: string; maxTemp?: number; minTemp?: number; weather?: string }
  • getFirstDaySlots(districtName: string): any[]

    • 建議 → 改成 WeatherSlot[]
  • getWeatherByDistrictGrouped(districtName: string): { [date: string]: any[] }

    • 建議 → { [date: string]: WeatherSlot[] }
  • getWeatherByLocationAny(location: string)

    • 這個你用 any 還 ok,因為是「整包丟給 chart/table」的 raw data,未來若只取部分再訂也行。

❌ 可以省略 interface(讓 TS 自動推斷就好)

  • isLoading: boolean = true;
  • campSites: CampSite[] = [];(這個其實 TS 會推斷出來,寫 : CampSite[] 算是多餘的,但保留也沒問題)
  • campSiteSearchResults: CampSite[] = []; 同上
  • campDistData: CampDistData[] = []; 同上

這些小狀態,TS 自己會推斷,寫不寫型別都行。


🎯 總結 (給你一個規則表)

類型 要不要 interface/type
API Response 要,至少定義會顯示在畫面的欄位
CSV 轉換後的資料 (CampSite)
表單資料 (CampSearch)
跨頁共用的資料 (CampDistData, WeatherAPI)
畫面 UI 狀態 (isLoading, flag, 小字串) 不用,交給 TS 推斷
一次性 function local 物件 不用
暫時不清楚的資料 可以先 any,後面再抽型別

👉 如果你要「快速先跑」,我會建議:

  • weatherlocationWeather → 先留 any
  • 其他 API / 表單 / CampSite → 全部要 interface
  • 專案穩定後再慢慢把 weather 抽型別出來

[Day06] Typescript 排序套件庫Lodash

在處理陣列物件時常會遇到需要排序陣列的問題,而用sort 得搭配 reduce,寫法醜、不好閱讀。
所以就詢問看看AI有何通用套件在大的專案裡常用。

目前是Lodash(最常用),閱讀起來也很直觀。

目前有三種

  1. Lodash(最常用)

Lodash安裝

1
npm install lodash

單屬性排序.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import _ from "lodash";

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

const users: User[] = [
{ id: 3, name: "Tom", age: 28 },
{ id: 1, name: "Amy", age: 22 },
{ id: 2, name: "John", age: 30 },
];

// 單屬性排序
const byAge = _.orderBy(users, ["age"], ["asc"]); // 升冪
const byName = _.orderBy(users, ["name"], ["desc"]); // 降冪

console.log(byAge);

多屬性排序.ts

1
const multiSort = _.orderBy(users, ["age", "name"], ["asc", "desc"]);
  1. Remeda(型別安全版 Lodash)

安裝remeda

1
npm install remeda
1
2
3
4
import { sortBy } from "remeda";

const byAge = sortBy(users, (u) => u.age);
const byName = sortBy(users, (u) => u.name);

3.內建(ES2023 新增,原生) toSorted

但是寫法還是有點不好閱讀,多排的時候

1
const byAge = users.toSorted((a, b) => a.age - b.age);

[Day05] Typescript 取得資料後進行的處理

這回主要著重在Typescript取得資料後進行的處理
主要介紹兩種處理:

  1. JSON與JSON合併
  2. 巢狀JSON內屬性為undefined時存取

實作一:JSON與JSON合併

有user、userDetail,將相同userId的資料做合併

json檔案

1
2
3
4
5
6
7
8
9
10
11
// user.json
[
{ "userId": "1", "name": "LIN" },
{ "userId": "2", "name": "DES" }
]

// userDetail.json
[
{ "userId": "1", "age": 29, "city": "TAIPEI" },
{ "userId": "2", "age": 20, "city": "TAIPEI" }
]

typescript實作資料合併:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
interface User {
userId: string;
name: string;
}

interface UserDetail {
userId: string;
age: number;
city: string;
}
interface UserWithDetail {
userId: string;
name: string;
age: number;
city: string;
}
const users: User[] = [
{ userId: "1", name: "LIN" },
{ userId: "2", name: "DES" },
];

const userDetails: UserDetail[] = [
{ userId: "1", age: 29, city: "TAIPEI" },
{ userId: "2", age: 20, city: "TAIPEI" },
];

// 合併
const usersWithDetails : UserWithDetail[] = users.map(user => {
// 合併,若找不到給空 detail 符合介面屬性
const detail = userDetails.find(d => d.userId === user.userId) || { age: 0, city: "" };;
return { ...user, ...detail };
});

/*
users.map(...) 會遍歷 users 陣列,每個使用者跑一次 callback。
在 callback 裡,用 .find(...) 去 userDetails 陣列裡找符合的 userId。
.find() 每次可能要從頭掃到尾,最糟情況要檢查整個陣列。
最後用展開運算子 {...user, ...detail} 合併成一個新物件。
缺點:
如果 users 有 n 筆,userDetails 有 m 筆
.map() 外層跑 n 次,每次 .find() 要在 m 筆資料裡找
總時間複雜度 = O(n × m)
如果 n=10,000,m=10,000 → 需要大約 1 億次比對(非常慢)
*/


console.log(usersWithDetails);

改善更好作法:
typescript實作資料合併:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
interface User {
userId: string;
name: string;
}

interface UserDetail {
userId: string;
age: number;
city: string;
}

interface UserWithDetail {
userId: string;
name: string;
age: number;
city: string;
}
const users: User[] = [
{ userId: "1", name: "LIN" },
{ userId: "2", name: "DES" },
];

const userDetails: UserDetail[] = [
{ userId: "1", age: 29, city: "TAIPEI" },
{ userId: "2", age: 20, city: "TAIPEI" },
];

// 先用 users 建立 Map
// 這裡用 Partial<UserDetail> 的目的,是 允許在 Map 裡的 user 物件還沒有完整的 UserDetail 時,也能先存入 Map,而不會 TypeScript 報錯。
const userMap = new Map<string, User & Partial<UserDetail>>(
users.map(user => [user.userId, { ...user }])
);

// 把詳細資料合併進去
userDetails.forEach(detail => {
const user = userMap.get(detail.userId);
if (user) {
userMap.set(detail.userId, { ...user, ...detail });
}
});
// 最後轉回陣列
const usersWithDetails: UserWithDetail[] = Array.from(userMap.values());

console.log(usersWithDetails);

/*
[
{ userId: '1', name: 'LIN', age: 29, city: 'TAIPEI' },
{ userId: '2', name: 'DES', age: 20, city: 'TAIPEI' }
]
*/

/*
優點:

1.用 Map 做為索引,查找效率高
把 users 陣列先轉成 Map,以 userId 當 key
查找一個 user 是否存在,只需 O(1),比起用 users.find(...) 的 O(n) 快很多
優點:當 users 或 userDetails 很大時,效能差異非常明顯

2.直接把 detail 複製進 user 物件,簡潔明瞭
使用Map.set保持原資料不被修改(透過展開建立新物件)
適用於 Immutable 思維(對 React / Vue 等框架更安全)
可輕鬆擴展多個 detail 陣列

3.把 Map 的 value 轉成陣列,非常直觀
Array.from 時間 O(n) 空間 O(n)
不用額外 .forEach 或 .map
代碼簡潔,可讀性高
*/


實作二:巢狀JSON內屬性為undefined時存取

假設有以下資料,而某些物件數值為undefined,該如何避免runtime存取問題。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
interface Address {
type: string;
city: string;
}

interface UserDetail {
age: number;
address: Address;
}

interface User {
userId: string;
name: string;
userDetail?: UserDetail; // 可選
// userDetail? 是 可選屬性,它的型別預設就是 UserDetail | undefined
}

/*
必填欄位 → TypeScript 要求一定要給值
nullable/optional 欄位 → 可以用 ?. 或判斷安全存取
*/

const user1: User = {
userId: "1",
name: "LIN",
userDetail: {
age: 29,
address: { type: "home", city: "TAIPEI" },
},
};

const user2: User = {
userId: "2",
name: "DES",
// userDetail: undefined // 可省,也等於undefined
};

const user1addresstype = user1.userDetail?.address?.type;
const user2addresstype = user2.userDetail?.address?.type; // 安全,會是 undefined

userDetail 在 User 介面裡是 可選的 (?)
所以 user2 沒有 userDetail 也符合型別
存取時使用了 optional chaining (?.)
user1addresstype → “home”
user2addresstype → undefined(因為 userDetail 不存在

避免了必填欄位缺失的型別錯誤
安全存取巢狀欄位
如果要進一步操作 user2addresstype,需要考慮可能是 undefined

在 TypeScript/JavaScript 裡,?.(optional chaining)運作的原理就是 如果左邊是 null 或 undefined,就直接回傳 undefined 而不是繼續存取,所以不會拋錯。
後面的 .address 和 .type 不會執行 → 沒有 runtime error

[Day04] Typescript 呼叫API整理

這回來是API呼叫方式
常見的API在大型專案中都是會先封裝好,再透過API網址去呼叫。

主要使用axios,因為可以控制版本,相對於原生的fetch而言。

該份API呼叫範例程式碼優點:
1.集中管理 API 主機 URL
2.呼叫端程式碼更乾淨
3.易於維護和擴展
4.支援多環境
5.安全性
6.未來擴充方便

「配置集中化 + 呼叫簡潔化 + 易維護 + 安全 + 支援多環境」

1
2
3
4
5
# API呼叫
npm install axios
npm install --save-dev @types/axios
# API環境檔案
npm install dotenv

首先環境檔案

.env

1
2
3
4
API_BASE_URL=https://jsonplaceholder.typicode.com
API_USERS_PATH=/users
API_POSTS_PATH=/posts
API_COMMENTS_PATH=/comments

HttpClient.ts
把 GET 和 POST 都包成 HttpClient 的方法,並保留 TypeScript 泛型支援

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// HttpClient.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import dotenv from 'dotenv';

dotenv.config(); // 讀取 .env

class HttpClient {
private client: AxiosInstance;

constructor() {
const baseURL = process.env.API_BASE_URL || '';
this.client = axios.create({
baseURL,
timeout: 5000,
headers: { 'Content-Type': 'application/json' },
});
}

public async getData<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.get<T>(url, config);
return response.data;
}

public async postData<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.client.post<T>(url, data, config);
return response.data;
}
}

export default HttpClient;

型別定義

type.ts

1
2
3
4
5
6
7
8
9
10
11
// types.ts
export interface User {
id: number;
name: string;
email: string;
}

export interface CreateUserRequest {
name: string;
email: string;
}

呼叫範例:
main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// main.ts
import dotenv from 'dotenv';
dotenv.config(); // 讀取 .env

import HttpClient from './HttpClient';
import { User, CreateUserRequest } from './types';

const api = new HttpClient(); // 不用傳 baseURL,已經在 HttpClient 裡讀取

async function run() {
// GET 所有使用者
const users: User[] = await api.getData<User[]>(process.env.API_USERS_PATH || '/users');
console.log('Users:', users);

// GET 單一使用者
const user: User = await api.getData<User>(`${process.env.API_USERS_PATH}/1`);
console.log('User 1:', user);

// POST 新使用者
const newUserData: CreateUserRequest = { name: 'John Doe', email: 'john@example.com' };
const newUser: User = await api.postData<User>(process.env.API_USERS_PATH || '/users', newUserData);
console.log('New User:', newUser);
}

run();

總結:
HttpClient 自動抓 .env 的 API_BASE_URL,呼叫時不用再傳。
.env 仍然可以管理不同環境的 URL 與 API 路徑。
main.ts 只要 import HttpClient + 用 .env 定義的路徑即可。

Javascript const排解

這是一個常見又很重要的 JavaScript 問題:


❓為什麼用 const 宣告的物件可以 push

1
2
3
const arr = [1, 2, 3];
arr.push(4);
console.log(arr); // [1, 2, 3, 4] ✅

✅ 答案:因為 const 限制的是 變數的參考(reference)不可重新指定不是內容不可改變!


🔍 詳細說明:

  • 在 JavaScript 中,物件(Object)與陣列(Array)是「參考型別(reference type)」。
  • const 宣告的是「變數的參考地址不能變動」,但這個地址裡的內容(物件屬性、陣列元素)是可以改的

✅ 可做的事:

可以修改物件/陣列的內容

1
2
3
4
5
6
const obj = { a: 1 };
obj.a = 2; // ✅ 可以改屬性
obj.b = 3; // ✅ 可以新增屬性

const arr = [1, 2];
arr.push(3); // ✅ 可以修改內容(但不是變數本身)

❌ 不可做的事:

不能把這個變數重新指向其他物件或值

1
2
3
4
5
const obj = { a: 1 };
obj = { b: 2 }; // ❌ TypeError:不能重新指派 const 變數

const arr = [1, 2];
arr = [3, 4]; // ❌ 同上,不能改 reference

🧠 小結

類型 可以改內容? 可以重新指定變數?
const 物件 ✅ 可以 ❌ 不行
let 物件 ✅ 可以 ✅ 可以
var 物件 ✅ 可以 ✅ 可以

📌 延伸說明(常見誤解)

❗誤解:「const 宣告就是不可變的變數」

錯。const 宣告的變數是不可被重新賦值,但如果變數是物件或陣列,其內容仍可修改。

如果你想讓整個物件內容都不能變動,可以使用:

1
2
3
const obj = Object.freeze({ a: 1 });
obj.a = 2; // 不會報錯,但不會改成功(嚴格模式下會報錯)
console.log(obj.a); // 1

[Day03] Typescript面試選擇30題

來到第三天,這回用AI生成了常見的Typescript面試選擇題。
一共30題,現在使用AI確實比過往一個一個爬stackoverflow好很多了。

涵蓋基礎、進階、型別系統與實務應用


TypeScript 面試選擇題(共 30 題)

基礎語法與型別

  1. 以下哪一個是正確的 TypeScript 型別宣告?
    A. let age: number = '25';
    B. let age: string = 25;
    C. let age: number = 25;
    D. let age: any = 25;

    解析number 不能賦值為字串,C 正確。D:(語法正確,但不建議) 型別標註 any,可以接受任何型別,但失去型別檢查的優勢。很多 TypeScript 規範會禁止 any


  1. unknown 型別的特性是什麼?
    A. 可以直接做任何運算
    B. 必須先進行型別檢查後才能使用 ✅
    C. 與 any 完全相同
    D. 只能賦值為 null

    解析unknownany 更安全,需要型別收窄後才能操作。
    unknown = “我不知道它是什麼型別,所以你得先確認再用”
    any = “我懶得檢查,全部放行”

1
2
3
4
5
6
7
8
9
10
11
12
    let u: unknown = "Hello";

// 錯誤:不能直接調用方法
// console.log(u.toUpperCase()); ❌

// 必須檢查型別
if (typeof u === "string") {
console.log(u.toUpperCase()); // ✅
}

// 或使用型別斷言
console.log((u as string).toUpperCase()); // ✅

  1. 哪一個關鍵字用來定義常數 enum?
    A. enum
    B. const enum
    C. static enum
    D. readonly enum

    解析const enum 在編譯時會被內聯,效能更好。


  1. 下面哪種是交叉型別(Intersection Types)的正確語法?
    A. type Person = { name: string } && { age: number };
    B. type Person = { name: string } | { age: number };
    C. type Person = { name: string } & { age: number };
    D. type Person = { name: string, age: number };

    解析:交叉型別用 &,代表合併兩個型別。


  1. 下列哪個是泛型函式的正確定義?
    A. function identity<T>(arg: T): T { return arg; }
    B. function identity(arg: T): T { return arg; }
    C. function identity<T>(T arg) { return arg; }
    D. function identity(arg) { return arg; }

進階型別系統

  1. Partial<T> 工具型別的功能是什麼?
    A. 把 T 所有屬性變成必填
    B. 把 T 所有屬性變成可選 ✅
    C. 移除 T 的所有屬性
    D. 把 T 轉換成陣列

  1. 下列哪一個是索引型別查詢的正確用法?
    A. type Keys = keyof Person;
    B. type Keys = valueof Person;
    C. type Keys = indexof Person;
    D. type Keys = typeof Person;

等同於

1
2
3
type Keys = keyof Person;
// 等同於:type Keys = "name" | "age" | "address"


  1. never 型別的用途是什麼?
    A. 表示一個函式永遠不會回傳 ✅
    B. 表示一個變數可以是任何型別
    C. 表示一個變數為空
    D. 表示一個變數尚未初始化

  1. 下列哪個範例正確使用 as const
    A. const arr = [1, 2, 3] as const;
    B. let arr = [1, 2, 3] as const;
    C. const arr as const = [1, 2, 3];
    D. const arr = as const [1, 2, 3];

as const 的作用是把一個值斷言為最窄的型別(literal type),而且會把它變成 readonly。
元素型別不再是 number,而是字面值型別 → 第一個元素就是 1、第二個是 2、第三個是 3

陣列變成唯讀(readonly) → 不能改值或 push

1
2
arr[0] = 99; // ❌ 錯誤:Cannot assign to '0' because it is a read-only property
arr.push(4); // ❌ 錯誤:Property 'push' does not exist on type 'readonly [1, 2, 3]'

  1. 哪一種方式可以防止物件被修改?
    A. readonly
    B. private
    C. const
    D. static

const → 只能防止變數的重新指派,但物件內容還是可以改:

1
2
const obj = { name: "Tom" };
obj.name = "Jerry"; // ✅ 可以改

readonly 可用在物件屬性上(interface 或 type)、類別屬性上

1
2
3
4
5
6
7
8
interface User {
readonly id: number;
name: string;
}

const user: User = { id: 1, name: "Alice" };
user.id = 2; // ❌ 錯誤,readonly 屬性不能改
user.name = "Bob"; // ✅ 可以改

函式與 OOP

  1. TypeScript 中,介面(interface)可以
    A. 只用來定義物件形狀 ✅
    B. 被類別實作 ✅
    C. 被擴展(extend) ✅
    D. 以上皆是 ✅

  1. 函式的可選參數應該放在
    A. 最前面
    B. 中間
    C. 最後面 ✅
    D. 任意位置

最後面

1
2
3
4
5
6
7
8
9
10
11
// 正確範例:可選參數放最後面
function greet(name: string, age?: number) {
if (age !== undefined) {
console.log(`Hello ${name}, you are ${age} years old.`);
} else {
console.log(`Hello ${name}`);
}
}

greet("Alice"); // OK
greet("Bob", 30); // OK

  1. 下列哪個範例正確定義函式多載(Overload)?
    A.✅

    1
    2
    3
    function add(a: number, b: number): number;
    function add(a: string, b: string): string;
    function add(a: any, b: any) { return a + b; }

    B.

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

    C.

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

    D. 以上皆非

函式多載(Function Overloading)是指在同一個函式名稱底下,定義多種不同的參數型態與數量,讓函式能根據呼叫時傳入的參數型別或數量,執行不同的邏輯或回傳不同型別的結果。


  1. abstract 關鍵字的用途是什麼?
    A. 定義不能被實例化的類別 ✅
    B. 定義常數
    C. 定義不可修改的變數
    D. 定義泛型

  1. 以下哪個是介面擴展的正確方式?
    A. interface B extends A {}
    B. interface B implements A {}
    C. class B extends A {}
    D. type B = A extends {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 先定義介面 A
interface A {
name: string;
age: number;
}

// B 繼承 A,新增一個屬性 job
interface B extends A {
job: string;
}

// 使用範例
const person: B = {
name: "Alice",
age: 30,
job: "Engineer",
};

console.log(person.name); // Alice
console.log(person.job); // Engineer

實務應用與錯誤檢查

  1. TypeScript 中 strictNullChecks 開啟後會
    A. 禁止變數為 nullundefined 除非型別允許 ✅
    B. 自動轉換 null 為空字串
    C. 忽略 null
    D. 允許任何型別

  1. tsconfig.jsonnoImplicitAny 的作用是
    A. 允許隱含 any
    B. 禁止隱含 any
    C. 自動推斷 any
    D. 強制使用 unknown

  1. 下列哪一種是型別守衛(Type Guard)?
    A. typeof value === 'string'
    B. console.log(value)
    C. value.length
    D. value instanceof

型別守衛(Type Guard)是 TypeScript 裡用來在程式碼執行時檢查變數型別的一種技術。它可以讓編譯器根據條件判斷,縮小變數的型別範圍,從而安全地訪問該型別的屬性或方法。
typeof value === ‘string’
利用 typeof 運算子判斷原始型別,常用於檢查 string、number、boolean 等基本型別,是一種典型的型別守衛。
value instanceof SomeClass
利用 instanceof 判斷物件是否是某個類別的實例,也是典型的型別守衛。


  1. 哪個工具型別會移除指定屬性?
    A. Pick<T, K>
    B. Omit<T, K>
    C. Partial<T>
    D. Exclude<T, K>
    這四個都是 TypeScript 內建的 工具型別(Utility Types),用來操作和轉換型別,讓開發更方便靈活。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//1.
interface Person {
name: string;
age: number;
address: string;
}

type PersonNameAge = Pick<Person, "name" | "age">;
// 等同於 { name: string; age: number; }

//2.
type PersonWithoutAddress = Omit<Person, "address">;
// 等同於 { name: string; age: number; }

//3.
type PartialPerson = Partial<Person>;
// 等同於 { name?: string; age?: number; address?: string; }

//4.
type T1 = "a" | "b" | "c";
type T2 = Exclude<T1, "a" | "b">;
// 結果是 "c"


  1. 下列哪個是 enum 的預設值?
    A. 從 0 開始遞增 ✅
    B. 從 1 開始遞增
    C. 必須手動指定
    D. 沒有預設值

混合進階題

  1. infer 關鍵字用於
    A. 在條件型別中推斷型別 ✅
    B. 宣告泛型
    C. 定義類別
    D. 宣告常數

infer 是 TypeScript 的關鍵字,只能用在 條件型別(conditional types) 裡面,
用來 從型別中「推斷(infer)」某個子型別,然後讓你在條件型別的「真分支」使用這個推斷出來的型別。

1
2
3
4
5
6
7
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function fn() {
return 123;
}

type Result = ReturnType<typeof fn>; // Result = number

  1. 哪個選項正確使用了條件型別?
    A. type IsString<T> = T extends string ? true : false;
    B. type IsString<T> = string extends T ? true : false;
    C. type IsString<T> = T ? true : false;
    D. 以上皆非

  1. 下列哪個型別表示「除了 T 以外的所有型別」?
    A. Exclude<U, T>
    B. Extract<U, T>
    C. Omit<U, T>
    D. Partial<T>

  1. 以下程式碼輸出什麼?

    1
    2
    enum Color { Red, Green, Blue }
    console.log(Color[0]);

    A. 0
    B. 'Red'
    C. '0'
    D. undefined


  1. TypeScript 編譯器的輸出是什麼?
    A. .ts 檔案
    B. .d.ts 檔案
    C. .js 檔案 ✅
    D. .tsconfig 檔案

  1. 哪個修飾子可以讓類別屬性在繼承中可用,但在外部不可存取?
    A. private
    B. protected
    C. public
    D. readonly

  1. 下列哪個不是型別別名(Type Alias)的用途?
    A. 為複雜型別取別名
    B. 定義物件形狀
    C. 建立聯合型別
    D. 被類別實作 ✅

  1. TypeScript 中模組預設匯出的正確寫法是
    A. export = MyClass;
    B. export default MyClass;
    C. module.exports = MyClass;
    D. exports.MyClass = MyClass;

  1. 下列哪個是非同步函式的正確寫法?
    A. function getData(): Promise<string> {}
    B. async function getData(): Promise<string> {}
    C. function getData(): string {}
    D. await function getData() {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 非同步函式,回傳 Promise<string>
async function getData(): Promise<string> {
// 模擬非同步操作,例如從伺服器取得資料
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("這是取得的資料");
}, 1000);
});
}

// 使用範例
async function main() {
console.log("開始取得資料...");
const data = await getData();
console.log("取得資料:", data);
}

main();

  1. TypeScript 型別檔案的副檔名是
    A. .ts
    B. .js
    C. .d.ts
    D. .type.ts

.d.ts 檔案(宣告檔)
只包含型別宣告(Type Declarations),不包含實作。
.ts 檔案
定義程式碼的實作,包含函式、類別、變數等。
是你寫邏輯和功能的主要地方。
編譯時會被轉成 JavaScript。


補充複選題:
以下哪些是 TypeScript 中內建的型別(Built-in Types)?(可複選,並說明它們的用途)

好,我幫你把剛剛那題複選題改成面試題完整格式,
並且在每個正確答案後面加上 使用場景說明,方便記憶與考試時快速回想。


題目:
以下哪些是 TypeScript 中內建的型別(Built-in Types)?(可複選,並說明它們的用途)


A. string
 用來表示文字資料,例如:
 ts  let name: string = "Alice";  

B. number
 用來表示數字(整數與浮點數),例如:
 ts  let age: number = 25;  

C. boolean
 表示布林值(true/false),例如:
 ts  let isActive: boolean = true;  

D. symbol
 表示唯一且不可變的值(常用於物件屬性 key),例如:
 ts  const id: symbol = Symbol("id");  

E. bigint
 表示任意精度的整數(大於 Number.MAX_SAFE_INTEGER 時使用),例如:
 ts  let big: bigint = 9007199254740991n;  

F. null
 表示空值(需在 --strictNullChecks 關閉時能直接賦值),例如:
 ts  let nothing: null = null;  

G. undefined
 表示未定義的值,變數未賦值時的預設值,例如:
 ts  let notAssigned: undefined = undefined;  

H. void
 表示函式不返回值,例如:
 ts  function logMessage(): void {   console.log("Hello");  }  

I. never
 表示永遠不會有返回值(例如函式會拋出錯誤或無限循環),例如:
 ts  function throwError(): never {   throw new Error("Error!");  }  

J. object
 表示非原始型別的值,例如物件、陣列、函式等:
 ts  let person: object = { name: "Bob" };  

K. any
 表示不檢查型別(會失去型別安全),例如:
 ts  let data: any = 123;  data = "Hello";  

L. unknown
 表示未知型別(比 any 安全,必須先檢查型別才能使用),例如:
 ts  let value: unknown = "Hello";  if (typeof value === "string") {   console.log(value.toUpperCase());  }  

M. Function
 表示任何函式型別,例如:
 ts  let fn: Function = () => console.log("Run");  

N. Array
 表示陣列(同時可以寫成 T[]),例如:
 ts  let list: Array<number> = [1, 2, 3];  

O. tuple
 表示固定長度且每個元素型別已知的陣列,例如:
 ts  let tuple: [string, number] = ["Age", 25];  

P. Record
 (工具型別,不是基本型別)用於建立具有特定 key 與 value 型別的物件,例如:
 ts  type User = Record<"id" | "name", string>;  

Q. enum
 列舉型別,用於定義一組具名常數,例如:
 ts  enum Direction { Up, Down, Left, Right }  

[Day02] Typescript實作強型別todolist

這回主要使用Typescript時做一個todolist,以強型別方式規範資料,並操作CRUD
以過往實作經驗搭配AI撰寫,在職場正式專案上也很通用的做法

功能

  1. 新增(create)
  2. 查詢(readAll, readById)
  3. 更新(update)
  4. 刪除(delete)
  5. enum 狀態對應文字取得(getStatusName)

優點

1.可讀性高:
透過 StatusCode.Done、StatusCode.Pending 等命名,
代碼一目了然,比單純用 1、2、3 更好理解。
反向查詢文字 StatusCode[status] 讓狀態展示更友善。
enum 支援數字 ↔ 字串互轉,
程式內可用數字節省空間,UI 顯示可用文字增強可讀性。

2.維護方便:
CRUD 邏輯封裝在 TodoManager 類別中,讓資料操作統一管理,易於擴展與維護。

3.封裝良好:
todos 陣列設為私有 (private),外部只能透過方法操作資料,

4.型別安全:
status 欄位被定義成 StatusCode enum,
只能接受預先定義的數值(1 | 2 | 3),避免亂輸入或寫錯字串。
編譯器會檢查不合規的狀態賦值,降低執行錯誤風險。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// 狀態 enum
enum StatusCode {
Done = 1,
Pending = 2,
InProgress = 3
}

// 中文對應
const StatusCodeNameChi: Record<StatusCode, string> = {
[StatusCode.Done]: '完成',
[StatusCode.Pending]: '等待',
[StatusCode.InProgress]: '進行中'
};

// 英文對應
const StatusCodeNameEng: Record<StatusCode, string> = {
[StatusCode.Done]: 'Done',
[StatusCode.Pending]: 'Pending',
[StatusCode.InProgress]: 'In Progress'
};

// Todo 型別
interface Todo {
id: number;
nametitle: string;
status: StatusCode;
}

class TodoManager {
private todos: Todo[] = [];

// 新增
create(todo: Todo): void {
this.todos.push(todo);
}

// 讀取全部
readAll(): Todo[] {
return [...this.todos];
}

// 讀取單一
readById(id: number): Todo | undefined {
return this.todos.find(t => t.id === id);
}

// 更新
update(id: number, updatedFields: Partial<Todo>): boolean {
const index = this.todos.findIndex(t => t.id === id);
if (index === -1) return false;
this.todos[index] = { ...this.todos[index], ...updatedFields };
return true;
}

// 刪除
delete(id: number): boolean {
const originalLength = this.todos.length;
this.todos = this.todos.filter(t => t.id !== id);
return this.todos.length < originalLength;
}

// 取得英文狀態名稱
getStatusNameEng(status: StatusCode): string {
return StatusCodeNameEng[status];
}

// 取得中文狀態名稱
getStatusNameChi(status: StatusCode): string {
return StatusCodeNameChi[status];
}
}

// 測試 CRUD
const manager = new TodoManager();

manager.create({ id: 1, nametitle: 'Test Task 1', status: StatusCode.Pending });
manager.create({ id: 2, nametitle: 'Test Task 2', status: StatusCode.InProgress });

console.log('All Todos:', manager.readAll());

console.log('Todo with id=1:', manager.readById(1));

manager.update(1, { status: StatusCode.Done, nametitle: 'Updated Task 1' });
console.log('After Update:', manager.readById(1));

manager.delete(2);
console.log('After Delete:', manager.readAll());

const todo = manager.readById(1);
if (todo) {
console.log('英文狀態:', manager.getStatusNameEng(todo.status));
console.log('中文狀態:', manager.getStatusNameChi(todo.status));
}