[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 定義的路徑即可。

[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));
}

[Day01] Typescript與Javascript差異

因為未來工作長期會使用到Typescript,所以就趁這時候筆記一下與JS的差異方面。
過去在寫Vue3、Angular時有大量接觸Typescript,就趁這時間重溫一下。

  1. 賦值

JS 是動態型別 → 錯誤在執行時才發現
TS 是靜態型別 → 編譯時就檢查出錯,減少 bug

JS

1
2
let age = 25;
age = "twenty-five"; // ✅ JS 不會報錯,可能導致執行期錯誤

TS

1
2
3
let age: number = 25;
age = "twenty-five";
// ❌ 編譯錯誤:Type 'string' is not assignable to type 'number'
  1. 函數參數與回傳型別
    TS 允許定義 參數型別 和 回傳型別,防止不合法輸入

TS

1
2
3
4
5
function add(a: number, b: number): number {
return a + b;
}
add(1, "2");
// ❌ Argument of type 'string' is not assignable to parameter of type 'number'

3.可選參數及預設值

TS 用 ? 宣告參數可選
搭配 nullish coalescing (??) 處理 null 或 undefined

1
2
3
4
function greet(name?: string) {
console.log(`Hello, ${name ?? "Guest"}`);
}
greet(); // Hello, Guest

JS則是用||處理

JS

1
2
3
4
function greet(name) {
console.log(`Hello, ${name || "Guest"}`);
}
greet(); // Hello, Guest

4.Interface

TS interface 能保證物件結構正確,JS 則只能在執行時才發現問題

TS

1
2
3
4
5
6
7
8
9
10
11
interface User {
name: string;
age: number;
}

function printUser(user: User) {
console.log(user.name, user.age);
}
printUser({ name: "Alex", age: 25 }); // ✅
printUser({ name: "Alex" });
// ❌ Property 'age' is missing in type ...

5.Enum vs. Magic String

Enum 可集中管理常量並提供型別檢查,避免拼錯問題
TS

1
2
3
4
5
6
7
8
9
10
enum Status {
Success = "success",
Fail = "fail"
}

const status: Status = Status.Success;

if (status === Status.Success) {
console.log("OK");
}

JS則無
JS

1
2
3
4
const status = "success"; // Magic string,容易拼錯
if (status === "succes") { // 拼錯不會報錯
console.log("OK");
}

6.參數多種型別檢查Union Types

TS:

1
2
3
4
5
6
7
function printId(id: number | string) {
console.log(id);
}
printId(123);
printId("abc");
printId(true);
// ❌ Argument of type 'boolean' is not assignable to parameter of type 'string | number'
  1. 泛型 (Generics)

any 會失去型別資訊
泛型保留型別,讓函數既安全又通用
TS:

1
2
3
4
5
6
function identity<T>(arg: T): T {
return arg;
}

const a = identity(123); // a: number
const b = identity("hi"); // b: string

一般JS不好確認型別
JS:

1
2
3
function identity(arg) {
return arg; // 無法知道回傳型別
}

8.非空斷言與 Optional Chaining

?. 可安全存取深層屬性
搭配 ! 非空斷言在確定不為空時跳過檢查
TS:

1
2
const user: any = {};
console.log(user?.profile?.name); // ✅ undefined

JS則無法
JS:

1
2
const user = {};
console.log(user.profile.name); // ❌ TypeError
  1. any vs. unknown
    什麼時候用 any,什麼時候用 unknown?

JS:

1
2
let value; // 無型別限制
value.foo(); // 可能出錯

any 跟 JS 一樣完全不檢查型別(危險)
unknown 要先檢查型別才可使用(安全)

TS:

1
2
3
4
5
6
7
let value: unknown;
value = "hello";
// value.toUpperCase(); ❌ 必須先做型別檢查

if (typeof value === "string") {
console.log(value.toUpperCase()); // ✅
}
  1. 類別 (Class) 的型別修飾符

JS 與 TS 在類別屬性可見性上的差異
JS:

1
2
3
4
5
class Person {
constructor(name) {
this.name = name; // 無法限制外部訪問
}
}

TS 提供 public(預設)、private、protected,可精確控制存取範圍

TS:

1
2
3
4
5
6
class Person {
private name: string; // 僅類別內可訪問
constructor(name: string) {
this.name = name;
}
}
  1. 型別別名(Type Alias)與複雜型別組合

大型專案中可以將複雜型別命名,方便重用與維護
TS:

1
2
3
4
type OrderStatus = "pending" | "shipped" | "delivered";
type Order = { id: number; status: OrderStatus };

function process(order: Order) { /* ... */ }
  1. Intersection Types(交叉型別)合併資料結構

JS:

1
2
// 只能動態合併物件
const person = Object.assign({}, { name: "Alex" }, { age: 25 });

交叉型別在多個模組資料合併時很常用(特別是 Redux / API 結果整合)

TS:

1
2
3
4
5
type Name = { name: string };
type Age = { age: number };
type Person = Name & Age; // 交叉型別

const person: Person = { name: "Alex", age: 25 };
  1. 型別守衛(Type Guards)

JS

1
2
3
function handle(val) {
if (val.start) val.start(); // 可能 undefined
}

大型專案裡經常用自訂型別守衛來精準縮小型別範圍

TS

1
2
3
4
5
6
7
8
9
function isFunction(value: unknown): value is Function {
return typeof value === "function";
}

function handle(val: unknown) {
if (isFunction(val)) {
val(); // ✅ 已縮小型別
}
}
  1. 宣告檔(.d.ts)與第三方函式庫型別

JS:

1
2
3
// 使用第三方庫時,無法獲得型別提示
import moment from "moment";
moment().format("YYYY-MM-DD"); // 沒 IntelliSense

大型專案幾乎都要用 .d.ts 來補齊沒有型別定義的第三方套件
TS:

1
2
3
// @types/moment 提供型別定義
import moment from "moment";
moment().format("YYYY-MM-DD"); // ✅ 型別提示 + 自動完成

15.readonly 與 Immutable 資料結構

readonly 對大型專案的設定檔、常量物件非常重要

Typescript

1
2
3
4
5
6
7
type Config = {
readonly port: number;
};

const config: Config = { port: 3000 };
config.port = 4000;
// ❌ Cannot assign to 'port' because it is a read-only property
  1. keyof 與型別安全的物件鍵名存取

JS

1
2
3
function getProp(obj, key) {
return obj[key]; // key 可能錯拼
}

keyof 確保傳入的屬性名稱是物件中真實存在的鍵名

TS

1
2
3
4
5
6
7
8
function getProp<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

const user = { id: 1, name: "Alex" };
getProp(user, "name"); // ✅
getProp(user, "age");
// ❌ Argument of type '"age"' is not assignable to parameter of type '"id" | "name"'
  1. Mapped Types(映射型別)

JS:

1
2
// 只能手動定義每個屬性為可選
const partialUser = { name: "Alex" };

大型專案中 Partial、Required、Readonly 等都是 Mapped Types 的應用

TS:

1
2
3
4
type User = { id: number; name: string; age: number };
type PartialUser = { [K in keyof User]?: User[K] };

const partialUser: PartialUser = { name: "Alex" };
  1. 模組與命名空間(Modules vs Namespaces)

JS:

1
2
// 無型別檢查的全域污染
window.myApp = {};

大型專案建議使用 ES Modules 搭配 TS 的型別系統管理命名與依賴

TS:

1
2
// 使用模組系統
export const myApp = {};
  1. 型別推斷與 as const
1
2
const roles = ["admin", "user"];

as const 在定義固定字面值陣列、物件時非常有用(例如權限、狀態碼)

1
2
3
const roles = ["admin", "user"] as const;
// roles: readonly ["admin", "user"]
type Role = typeof roles[number]; // "admin" | "user"
  1. Non-null Assertion(非空斷言)與嚴格模式

JS:

1
2
let el = document.getElementById("myDiv");
el.style.color = "red"; // el 可能是 null

嚴格模式 (strictNullChecks) 在大型專案中很重要,可以提早發現 null/undefined 錯誤

TS:

1
2
let el = document.getElementById("myDiv")!;
el.style.color = "red"; // ✅ 告訴 TS 這裡一定不為 null

C# Flatten data by Linq

When I work on the project, I usually run into a problem about flatting data.
Because the original data could in the nested property, I have to get it out.
The following is an example about nested properties, and the requirement is that I must get all OrderDetailNumber.

Finally, I found an easy method that is use SelectMany.

LINQ SelectMany

Projects each element of a sequence to an IEnumerable and flattens the resulting sequences into one sequence.

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
using System;
using System.Linq;
using System.Collections.Generic;

namespace FlattenResult
{
internal class Program
{
public class Order
{
public string OrderNumber { get; set; }

public List<OrderDetail> orderDetails { get; set; }

public class OrderDetail
{
public string OrderDetailNumber { get; set; }
}
}

static void Main(string[] args)
{
var orders = new List<Order>
{
new Order
{
OrderNumber = "202305121111",
orderDetails = new List<Order.OrderDetail>
{
new Order.OrderDetail
{
OrderDetailNumber = "OD0001",
},
new Order.OrderDetail
{
OrderDetailNumber = "OD0001",
}
}
},
new Order
{
OrderNumber = "202305121112",
orderDetails = new List<Order.OrderDetail>
{
new Order.OrderDetail
{
OrderDetailNumber = "OD0003",
},
new Order.OrderDetail
{
OrderDetailNumber = "OD0004",
}
}
}
};

var orderDetailNumbers = orders.SelectMany(s => s.orderDetails)
.Select(s => s.OrderDetailNumber)
.ToList();

orderDetailNumbers.ForEach(f =>
{
Console.WriteLine(f);
});
}
}
}

Output:

1
2
3
4
OD0001
OD0001
OD0003
OD0004

C# Use AutoMapper to map data model

When writing data to SQL Server, there is always a process about mapping model.
The process is a transformation, such as A model transforming B model.
If the model contains too many properties, the code could became very long.
So we can use a plugin, AutoMapper, to avoid that.

AutoMapper : https://docs.automapper.org/

The following is an example of implementation about converting PersonModel to PersonDto, which is a list convert.

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
using System;
using System.Collections.Generic;

using AutoMapper;

namespace AutoMapExample
{
public class Program
{
public class PersonModel
{
public string PersionalID { get; set; }
public string Name { get; set; }
public int? Age { get; set; }
}
public class PersonDto
{
public int? Id { get; set; }
public string Pid { get; set; }
public string Name { get; set; }
public int? Age { get; set; }
}
public static void Main()
{
var personlist = new List<PersonModel>
{
new PersonModel
{
PersionalID = "E111111111",
Name = "TOM",
Age = 20,
},
new PersonModel
{
PersionalID = "B111111111",
Name = "J.Cole",
Age = 26,
}
};

var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<PersonModel, PersonDto>()
.ForMember(dest => dest.Pid, opt => opt.MapFrom(src => src.PersionalID))
.ForMember(dest => dest.Id, opt => opt.Ignore());
});

// Check for missing mapped properties.
config.AssertConfigurationIsValid();

var mapper = config.CreateMapper();
var dtoList = mapper.Map<List<PersonModel>, List<PersonDto>>(personlist);

dtoList.ForEach(x =>
{
Console.WriteLine($"id:{x.Id},pid:{x.Pid},name:{x.Name},age:{x.Age}");
});
}
}
}

output

1
2
id:,pid:E111111111,name:TOM,age:20
id:,pid:B111111111,name:J.Cole,age:26

Because id was set to ignore, it’s null when the console prints them.

The following is the mapping of the different name properity. Due to the name of the properity is different so I have to define a rule to map, such as PersionalID => Pid.

1
ForMember(dest => dest.Pid, opt => opt.MapFrom(src => src.PersionalID))

The following is a check for missing mapped properties.

1
config.AssertConfigurationIsValid()

Use C# SemaphoreSlim to lock async functions

When writing an async function or method, I encountered a problem with accessing data one-by-one, such as starting 100000 async APIs, which could lead to bugs.

Below is a problem example. I expect the result to be “Sum:100000”, but the actual result is “Sum:99995” or other abnormal results.

Example 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace SemaphoreSlimExample;

public class Program
{
private static int _sum;

public static void Main(string[] args)
{
Task.WaitAll(Enumerable
.Range(0, 100000)
.Select(x => Add(1))
.ToArray());
Console.WriteLine($"Sum:{_sum}");
}

private static async Task Add(int num)
{
await Task.Run(() =>
{
_sum += num;
});
}
}

Result:

1
Sum:99995

To fix this problem, I added the SemaphoreSlim to code, and the result always is correct “Sum:100000”.
It helps us lock and limit the asynchronous function’s data to one-by-one.

What is Semaphoreslim:
https://learn.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim?view=net-7.0

Represents a lightweight alternative to Semaphore that limits the number of threads that can access a resource or pool of resources concurrently.

Example2:

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
namespace SemaphoreSlimExample;

public class Program
{
private static int _sum;
private static readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);

public static void Main(string[] args)
{
Task.WaitAll(Enumerable
.Range(0, 100000)
.Select(x => Add(1))
.ToArray());
Console.WriteLine($"Sum:{_sum}");
}

private static async Task Add(int num)
{
await locker.WaitAsync();

await Task.Run(() =>
{
_sum += num;
});

locker.Release();
}
}

Result:

1
Sum:100000