โหมดมืด
บทที่ 06 — Utility Types (type สำเร็จรูป)
⏱ ใช้เวลา 2–3 ชั่วโมง
บทนี้เรียนอะไร
ในบทที่ 05 เราเรียน generic — วิธีสร้าง type ที่ยืดหยุ่นด้วยตัวเอง บทนี้เราจะรู้จัก "generic สำเร็จรูป" ที่ TypeScript เตรียมไว้ให้แล้ว เรียกว่า Utility Types
ลองนึกภาพ — บ่อยครั้งเราอยาก "แปลง type ที่มีอยู่ ให้เป็น type ใหม่" เช่น:
- มี type
Userอยากได้ version ที่ "ทุก field ไม่บังคับ" (สำหรับฟอร์มแก้ไข) - มี type
Userอยากได้ version ที่ "ตัด fieldpasswordออก" (สำหรับส่งให้ frontend)
แทนที่จะเขียน type ใหม่ทั้งหมดด้วยมือ TypeScript มี utility type ที่ทำงานแปลงพวกนี้ให้ — เขียนสั้น และที่สำคัญ เมื่อ type ต้นทางเปลี่ยน type ที่แปลงจะเปลี่ยนตามอัตโนมัติ
บทนี้ utility type ที่ใช้บ่อยที่สุด 5 ตัวคือ Partial, Pick, Omit, Record, ReturnType — ให้เน้นพวกนี้
Part 1: Utility Types ที่แปลง object
เราจะเริ่มจากกลุ่มที่ใช้บ่อยที่สุด — กลุ่มที่ "แปลงรูปร่างของ object type" สมมติเรามี type นี้เป็นตัวอย่างหลัก:
typescript
type User = {
id: number;
name: string;
email: string;
password: string;
};1.1 Partial<T> — ทำให้ทุก field "ไม่บังคับ"
📌 Partial = "บางส่วน" — แปลง type ที่ทุก field บังคับ ให้กลายเป็น "ใส่หรือไม่ใส่ก็ได้" ทุก field
Partial<T> รับ type เข้ามา แล้วคืน type ใหม่ที่ ทุก field ใส่ ? (ไม่บังคับ):
typescript
type PartialUser = Partial<User>;
// ผลลัพธ์เหมือนเขียนว่า:
// {
// id?: number;
// name?: string;
// email?: string;
// password?: string;
// }ใช้ตอนไหน? — ตอนทำฟังก์ชัน "อัปเดตข้อมูลบางส่วน" ที่คนเรียกอาจส่งมาแค่บาง field:
typescript
function updateUser(user: User, changes: Partial<User>): User {
return { ...user, ...changes };
}
const alice: User = { id: 1, name: "Alice", email: "a@x.com", password: "123" };
updateUser(alice, { name: "Alicia" }); // ✅ ส่งมาแค่ name ก็ได้
updateUser(alice, { email: "new@x.com" }); // ✅ ส่งมาแค่ email ก็ได้ถ้าไม่มี Partial พารามิเตอร์ changes จะต้องมีครบทุก field — ซึ่งไม่สมเหตุสมผลสำหรับการ "อัปเดตบางส่วน"
1.2 Required<T> — ตรงข้ามกับ Partial
📌 Required = "บังคับ / จำเป็น" — ทำให้ทุก field "ต้องมี" (ตัด
?ทิ้งทั้งหมด)
ตรงข้ามกับ Partial — Required<T> แปลงทุก field ของ type ให้กลายเป็น "บังคับต้องมี" (ลบเครื่องหมาย ? ออกทั้งหมด) เหมาะเวลาต้องการให้แน่ใจว่าทุกค่าถูกกรอกครบ:
typescript
type Settings = {
theme?: string;
fontSize?: number;
};
type CompleteSettings = Required<Settings>;
// { theme: string; fontSize: number } ← ไม่มี ? แล้ว1.3 Readonly<T> — ทำให้ทุก field แก้ไม่ได้
📌 Readonly = "อ่านได้อย่างเดียว" — สร้างแล้วล็อก แก้ไขค่าหลังจากนั้นไม่ได้
typescript
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; ... }
const user: ReadonlyUser = { id: 1, name: "A", email: "a@x.com", password: "x" };
user.name = "B"; // ❌ Error! ทุก field เป็น readonlyใช้กับข้อมูลที่ "ไม่ควรถูกแก้หลังสร้าง"
1.4 Pick<T, Keys> — เลือกเฉพาะบาง field
📌 Pick = "หยิบ" — หยิบเฉพาะ field ที่เราต้องการ ทิ้งที่เหลือ
Pick "หยิบ" เฉพาะ field ที่เราระบุ ออกมาเป็น type ใหม่:
typescript
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string } ← เอามาแค่ id กับ namePick<User, "id" | "name"> อ่านว่า "จาก User หยิบเอาแค่ id และ name"
ใช้ตอนไหน? — เมื่ออยากได้ "ส่วนย่อย" ของ type ใหญ่ เช่น ข้อมูลย่อ ๆ ที่แสดงในรายการ
1.5 Omit<T, Keys> — ตัดบาง field ออก
📌 Omit = "ละ / ตัดทิ้ง" — ตัด field ที่ไม่อยากได้ออก เหลือที่เหลือทั้งหมด (ตรงข้ามกับ
Pick)
Omit ตรงข้ามกับ Pick — มัน "ตัด" field ที่ระบุออก เหลือที่เหลือทั้งหมด:
typescript
type SafeUser = Omit<User, "password">;
// { id: number; name: string; email: string } ← ตัด password ออกใช้ตอนไหน? — สถานการณ์คลาสสิกที่สุดคือ "ตัดข้อมูลลับออกก่อนส่งให้ frontend":
typescript
function getPublicProfile(user: User): Omit<User, "password"> {
const { password, ...rest } = user; // แยก password ออก
return rest;
}Pick vs Omit — เลือกอันไหน?
- ถ้าอยากเก็บ "ไม่กี่ field" จากของที่มีเยอะ → ใช้
Pick(ระบุที่อยากเก็บ) - ถ้าอยากตัด "ไม่กี่ field" ออกจากของที่มีเยอะ → ใช้
Omit(ระบุที่อยากตัด)
Part 2: Record — สร้าง object type จาก key และ value
📌 Record = "บันทึก / ตาราง" — สร้าง object type โดยกำหนดว่า "key หน้าตาแบบไหน" และ "value หน้าตาแบบไหน" — เหมือนตารางคู่ key-value
Record<Keys, Value> สร้าง object type โดยบอกว่า "key เป็นแบบไหน และ value เป็นแบบไหน"
2.1 Record กับ key ที่รู้แน่นอน
typescript
type RolePermissions = Record<"admin" | "editor" | "viewer", boolean>;
// ผลลัพธ์:
// {
// admin: boolean;
// editor: boolean;
// viewer: boolean;
// }
const permissions: RolePermissions = {
admin: true,
editor: true,
viewer: false,
};ข้อดี: ถ้าลืมใส่ key ตัวใดตัวหนึ่ง TypeScript จะฟ้อง — บังคับให้ครบ
2.2 Record กับ key แบบ string ทั่วไป
typescript
type ScoreBoard = Record<string, number>;
// เหมือน { [key: string]: number } — key เป็น string อะไรก็ได้, value เป็น number
const scores: ScoreBoard = {
alice: 90,
bob: 85,
};Record<string, number> คือวิธีเขียน index signature (การประกาศว่า "key เป็น string อะไรก็ได้ value เป็นชนิดนี้") ที่อ่านง่ายกว่ารูปแบบเดิม { [key: string]: number } ที่เจอในบทที่ 04 — ทั้งสองแบบให้ผลเหมือนกัน แต่คนนิยมใช้ Record มากกว่าเพราะอ่านง่ายกว่า
Part 3: Utility Types ที่ทำงานกับ Union
กลุ่มนี้ทำงานกับ union type (ที่เราเรียนในบทที่ 02)
3.1 Exclude<T, U> — เอาสมาชิกออกจาก union
Exclude<T, U> สร้าง union ใหม่โดยตัดสมาชิก U ออกจาก union T — เหมาะเวลาอยากได้ชุดย่อย (เช่นเอา role ทั้งหมดยกเว้น guest):
typescript
type Role = "admin" | "editor" | "viewer" | "guest";
type StaffRole = Exclude<Role, "guest">;
// "admin" | "editor" | "viewer" ← เอา "guest" ออกExclude<T, U> = "จาก union T เอาสมาชิกที่ตรงกับ U ออก"
3.2 Extract<T, U> — เก็บเฉพาะสมาชิกที่ตรง
typescript
type Role = "admin" | "editor" | "viewer" | "guest";
type PrivilegedRole = Extract<Role, "admin" | "editor">;
// "admin" | "editor" ← เก็บเฉพาะที่ตรงExtract ตรงข้ามกับ Exclude — มัน "เก็บเฉพาะ" สมาชิกที่ตรง
3.3 NonNullable<T> — เอา null และ undefined ออก
typescript
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string ← เอา null กับ undefined ออกใช้บ่อยเวลามี type ที่อาจเป็น null/undefined แล้วเราอยากได้ version ที่ "การันตีว่ามีค่า"
Part 4: Utility Types ที่ทำงานกับฟังก์ชัน
กลุ่มนี้ใช้ "ดึงข้อมูล type ออกจากฟังก์ชัน"
4.1 ReturnType<F> — ดึง type ของค่าที่ฟังก์ชันคืน
typescript
function createUser(name: string, age: number) {
return { id: Date.now(), name, age, createdAt: new Date() };
}
type NewUser = ReturnType<typeof createUser>;
// { id: number; name: string; age: number; createdAt: Date }อธิบาย — typeof createUser ดึง "type ของฟังก์ชัน" (จำบทที่ 03 ได้ไหม) แล้ว ReturnType<...> ดึง "type ของค่าที่มันคืน" ออกมาอีกที
ใช้ตอนไหน? — เมื่อฟังก์ชันคืน object ที่ซับซ้อน และเราอยากได้ type ของมันมาใช้ โดยไม่ต้องเขียน type ซ้ำ (ถ้าฟังก์ชันเปลี่ยน type นี้เปลี่ยนตามอัตโนมัติ)
4.2 Parameters<F> — ดึง type ของพารามิเตอร์
typescript
function sendMessage(to: string, body: string, urgent: boolean) {
/* ... */
}
type MsgParams = Parameters<typeof sendMessage>;
// [to: string, body: string, urgent: boolean] ← เป็น tupleParameters คืน type ของพารามิเตอร์ทั้งหมดเป็น tuple
4.3 Awaited<T> — ดึง type ที่ Promise ห่อไว้
Awaited<T> ใช้เพื่อ "แกะ" type ที่ซ้อนอยู่ใน Promise ออกมา เหมือนรอผล await แล้วถามว่าค่าที่ได้มีชนิดอะไร:
typescript
type Result = Awaited<Promise<string>>;
// string ← แกะ Promise ออก เหลือ stringAwaited "แกะ" Promise ออก — ถ้า Promise ซ้อนกันหลายชั้นมันก็แกะให้หมด มีประโยชน์เวลาทำงานกับ async function:
typescript
async function fetchUser() {
return { name: "Alice", age: 30 };
}
type User = Awaited<ReturnType<typeof fetchUser>>;
// { name: string; age: number }
// (ReturnType ได้ Promise<{...}> แล้ว Awaited แกะ Promise ออก)Part 5: Utility Types สำหรับ string
TypeScript มี utility type ที่แปลงตัวอักษรของ string literal type:
typescript
type Greeting = "hello world";
type Upper = Uppercase<Greeting>; // "HELLO WORLD"
type Lower = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello" (ตัวแรกเป็นพิมพ์ใหญ่)
type Uncap = Uncapitalize<"Hello">; // "hello" (ตัวแรกเป็นพิมพ์เล็ก)ใช้ไม่บ่อยในงานทั่วไป — มีประโยชน์มากตอนทำ type ขั้นสูง (บทที่ 10)
Part 6: เครื่องมือพื้นฐานที่ใช้สร้างทุกอย่าง — keyof, T[K], typeof
ก่อนจะไปต่อ ขอทบทวนและขยายเครื่องมือ 3 ตัวที่เป็นรากฐาน — เราเจอมาบ้างแล้ว (บทที่ 05 Part 5.3 อธิบาย keyof และ T[K] ครั้งแรก)
6.1 keyof — ดึงชื่อ key ทั้งหมด
keyof T ให้ union ของชื่อ key ทั้งหมดใน type — เช่น keyof User ได้ "name" | "age" | "email" ใช้บ่อยมากเป็นรากฐานของ generic ที่ทำงานกับ property:
typescript
type User = { name: string; age: number; email: string };
type UserKeys = keyof User;
// "name" | "age" | "email"6.2 Indexed Access T[K] — ดึง type ของ property
Indexed Access (การเข้าถึงด้วย index — พูดง่าย ๆ คือ "ขอ type ของ field นี้โดยระบุชื่อ") เขียนด้วย T[K] ดึง type ของ property ที่ระบุออกมา — เช่น User["name"] ได้ string เจาะหลายชั้นได้ (User["address"]["city"]) มีประโยชน์เวลาอยากอ้างอิง type ของ field โดยไม่เขียนซ้ำ:
typescript
type User = {
name: string;
address: { city: string; zip: string };
};
type NameType = User["name"]; // string
type CityType = User["address"]["city"]; // string ← เจาะลึกหลายชั้นได้6.3 typeof (ในโลกของ type) — ดึง type จากค่าที่มีอยู่
อันนี้ทรงพลังและมือใหม่มักไม่รู้จัก บางครั้งเรามี "ค่า" อยู่แล้ว และอยากได้ "type ของมัน":
typescript
const defaultConfig = {
url: "http://localhost",
timeout: 5000,
retries: 3,
};
type Config = typeof defaultConfig;
// { url: string; timeout: number; retries: number }แทนที่จะเขียน type Config ด้วยมือ เราสร้างค่าจริงก่อน แล้วใช้ typeof ดึง type ออกมา — ถ้าค่าเปลี่ยน type เปลี่ยนตาม
⚠️ อย่าสับสน —
typeofมี 2 ความหมายขึ้นกับว่าใช้ที่ไหน:
- ใช้ในโค้ดปกติ (runtime) เช่น
if (typeof x === "string")→ เช็กชนิดค่าตอนรัน- ใช้ในตำแหน่ง type เช่น
type T = typeof config→ ดึง type ของค่าเขียนเหมือนกัน แต่คนละเรื่อง — ดูจาก "ตำแหน่ง" ที่มันอยู่
6.4 รวมพลัง — keyof typeof
typescript
const config = { url: "...", timeout: 5000 };
type ConfigKey = keyof typeof config;
// "url" | "timeout"
// (typeof config ได้ type ของ object → keyof ดึง key ออกมา)pattern keyof typeof ใช้บ่อยมาก — "เอาค่า object ที่มีอยู่ มาดึงรายชื่อ key เป็น type"
💡 สังเกตว่า
keyof typeofได้ union ของ key ถูกต้องเสมอ ไม่ว่าจะใส่as constหรือไม่ —as constจำเป็นเฉพาะตอนอยากได้ value เป็น literal type (เหมือนใน Lab 3 ที่ทำให้RoutePathเป็น"/" | "/profile" | "/settings"แทนที่จะเป็นstringเฉย ๆ) ไม่ได้จำเป็นสำหรับดึง key
6.5 ดึง type สมาชิกจาก array ด้วย [number]
typescript
const colors = ["red", "green", "blue"] as const;
type Color = typeof colors[number];
// "red" | "green" | "blue"อธิบาย: typeof colors ได้ type ของ array จากนั้น [number] หมายถึง "เข้าถึงด้วย index ที่เป็น number ใด ๆ" — ผลคือ union ของ type สมาชิกทั้งหมด
pattern นี้คือวิธีสร้าง union literal จาก array — ใช้แทน enum ได้ดี (จำบทที่ 02 ได้ไหม)
Part 7: รู้จัก Mapped Type (เกริ่นนำ) และสร้าง Utility เอง
📌 Mapped Type = "ชนิดที่วนทำกับทุก key" — เหมือนเขียน loop
forแต่เป็นระดับ type: "สำหรับทุก key ใน object ทำอะไรสักอย่างกับมัน"
🚩 โซนขั้นสูง — ข้ามได้ Part นี้เริ่มแตะ "การสร้าง utility type เอง" ด้วย mapped type ซึ่งเป็นเทคนิคขั้นสูง มือใหม่ ข้ามไป Part 8 ก่อนได้ — แค่รู้ว่า "utility type สร้างจาก mapped type" ก็พอ ส่วนที่ต้องใช้จริงในงานประจำคือ utility สำเร็จรูปใน Part 1-5
utility type ที่ TypeScript เตรียมไว้ จริง ๆ แล้วสร้างจากเทคนิคชื่อ Mapped Type — มันคือ "การวนทำกับทุก key ของ object type"
7.1 หน้าตาของ Mapped Type
typescript
type AllStrings<T> = {
[K in keyof T]: string;
// ↑ "สำหรับทุก key K ใน keyof T" — วนทำทีละ key
};
type User = { name: string; age: number };
type StringifiedUser = AllStrings<User>;
// { name: string; age: string } ← ทุก field กลายเป็น stringส่วน [K in keyof T] อ่านว่า "วนทีละ key ของ T" — มันคือ "ลูป for" ในระดับ type
ความจริง Partial ก็สร้างแบบนี้ — ลองดูว่า TypeScript นิยาม Partial ยังไง:
typescript
type Partial<T> = {
[K in keyof T]?: T[K];
// ↑ ใส่ ? ให้ทุก key → ทุก field กลายเป็น optional
};แค่เติม ? เข้าไปในลูป ก็ได้ Partial แล้ว — ไม่ลึกลับเลย
แตกทีละชิ้น สำหรับมือใหม่ — [K in keyof T]?: T[K] มี 3 ส่วน:
text
[K in keyof T] ? : T[K]
└──────┬──────┘ └┬┘ └─┬─┘
"วนทุก key K | "type ของ property K
ใน T" | เอามาเหมือนเดิม"
"ใส่ ? ให้ทุก key
(optional)"[K in keyof T]= "วน loop ทีละ key" (Kคือชื่อ key ปัจจุบันในรอบ)?= "ใส่เครื่องหมายไม่บังคับให้ key นี้"T[K]= "type ของ propertyKในT" (จาก Part 6.2 — indexed access)
อ่านรวม: "สำหรับทุก key K ใน T ใส่ ? แล้วเก็บ type เดิมไว้"
7.2 สร้าง Utility ของตัวเอง
เมื่อเข้าใจ mapped type คุณสร้าง utility เองได้ ตัวอย่างที่มีประโยชน์ — Nullable (ทำให้ทุก field เป็น null ได้):
typescript
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type User = { name: string; age: number };
type NullableUser = Nullable<User>;
// { name: string | null; age: number | null }7.3 DeepPartial — และทำไมต้อง "recursive"
Partial ปกติทำให้ field "ชั้นบนสุด" เป็น optional แต่ถ้า field เป็น object ซ้อน มันไม่ลงไปข้างใน:
typescript
type Config = {
server: { port: number; host: string };
};
type P = Partial<Config>;
// { server?: { port: number; host: string } }
// server เป็น optional แต่ port กับ host ข้างในยังบังคับอยู่!ถ้าเราอยากให้ "ลึกลงไปทุกชั้น" เป็น optional ต้องเขียน utility ที่ "เรียกตัวเอง" (recursive):
🧠 อ่านเฉพาะมือเก๋า — ตัวอย่างต่อไปนี้ใช้ conditional type (
T extends U ? X : Y) ทบทวนได้ที่ บทที่ 05 Part 7 เป็นโซนขั้นสูง ถ้ายังไม่คุ้นข้ามได้ไม่เสียหาย —Partialปกติจาก Part 1.1 พอใช้งานทั่วไปแล้ว
typescript
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
// ↑ ถ้า field เป็น object → เรียก DeepPartial กับมันอีกที
};
type DP = DeepPartial<Config>;
// { server?: { port?: number; host?: string } } ← optional ทุกชั้นที่ต้อง recursive เพราะ object อาจซ้อนกันได้ไม่จำกัดชั้น — utility ต้อง "ดำดิ่งลงไป" ทุกชั้นเอง
⚠️ ข้อจำกัด:
Array,Date,Map,Setก็นับเป็น "object" เหมือนกัน (ทุกอย่างที่ไม่ใช่ primitive ถือเป็น object ใน TypeScript) ดังนั้นT[K] extends objectจะ match พวกนี้ด้วย ไม่ใช่แค่ plain object ที่เราตั้งใจ — ผลคือ:
DeepPartial<{ createdAt: Date }>จะทำให้createdAtกลายเป็น{ getTime?: () => number; ... }แทนที่จะเป็นDateเฉย ๆ (ผิดเจตนา)- วิธีแก้: กันเคส special types เหล่านี้ออกก่อน เช่น:
typescripttype DeepPartial<T> = { [K in keyof T]?: T[K] extends Date | Array<any> ? T[K] : T[K] extends object ? DeepPartial<T[K]> : T[K]; };
- หรือใช้ library ที่จัดการเคสพวกนี้ให้แล้ว เช่น
type-fest
🧠 ส่วนนี้เริ่มเข้าเขตขั้นสูง ถ้ายังงง mapped type ไม่ต้องกังวล — บทที่ 10 จะลงลึกเต็ม ๆ ในบทนี้แค่รู้ว่า "utility type สร้างจาก mapped type" และเข้าใจ utility สำเร็จรูปใน Part 1-5 ให้แม่นก็พอ
Part 8: Utility Types คู่กับ Zod
จำ Zod จากบทที่ 00 ได้ไหม — library ตรวจสอบข้อมูล runtime Zod เข้ากันได้ดีมากกับ utility type:
typescript
import { z } from "zod";
// 1. ประกาศ schema ครั้งเดียว
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
password: z.string(),
});
// 2. ดึง type ออกมาด้วย z.infer
type User = z.infer<typeof UserSchema>;
// { id: number; name: string; email: string; password: string }
// 3. Zod เองก็มี method แปลง schema คล้าย utility type
const PublicUserSchema = UserSchema.omit({ password: true });
type PublicUser = z.infer<typeof PublicUserSchema>;
// { id: number; name: string; email: string }ข้อดี: คุณเขียน schema ที่เดียว ได้ทั้ง "การตรวจสอบ runtime" และ "type ของ TypeScript" — ไม่ต้องดูแล 2 ที่ให้ตรงกัน เรื่องนี้จะลงลึกในบทที่ 11
Part 9: Lab — ลงมือทำ
Lab 1 — Pick และ Omit
ฝึกใช้ utility type สร้าง type ใหม่จากของเดิมในสถานการณ์จริง — Pick เลือกเฉพาะ field ที่ต้องการ, Omit ตัด field ความลับ (cost, supplier) ออกก่อนส่งให้ลูกค้า:
typescript
type Product = {
id: number;
name: string;
price: number;
cost: number; // ต้นทุน — ความลับ ห้ามส่งให้ลูกค้า
supplier: string; // ผู้ขายส่ง — ความลับ
};
// type สำหรับแสดงในรายการสินค้า — เอาแค่บาง field
type ProductCard = Pick<Product, "id" | "name" | "price">;
// type สำหรับส่งให้ลูกค้า — ตัดความลับออก
type PublicProduct = Omit<Product, "cost" | "supplier">;Lab 2 — Partial กับฟังก์ชันอัปเดต
ฝึกใช้ Partial<T> กับฟังก์ชันอัปเดต — รับเฉพาะ field ที่อยากเปลี่ยน (ไม่ต้องส่งครบทุกตัว) แล้ว merge (รวมโค้ด/รวมค่าเข้าด้วยกัน) เข้ากับค่าเดิม แพตเทิร์นที่เจอบ่อยมากใน API update:
typescript
type Settings = {
theme: "light" | "dark";
fontSize: number;
notifications: boolean;
};
function updateSettings(current: Settings, changes: Partial<Settings>): Settings {
return { ...current, ...changes };
}
const defaults: Settings = { theme: "light", fontSize: 14, notifications: true };
const updated = updateSettings(defaults, { theme: "dark" }); // ✅ ส่งแค่ themeLab 3 — Record และ keyof typeof
typescript
const ROUTES = {
home: "/",
profile: "/profile",
settings: "/settings",
} as const; // ⚠️ as const สำคัญ — ถ้าไม่ใส่ value จะ widen เป็น string ทั่วไป
type RouteName = keyof typeof ROUTES; // "home" | "profile" | "settings"
type RoutePath = typeof ROUTES[RouteName]; // "/" | "/profile" | "/settings"
function navigate(route: RouteName): void {
console.log(`ไปที่ ${ROUTES[route]}`);
}
navigate("home"); // ✅
navigate("dashboard"); // ❌ Error! ไม่มี route นี้⚠️ บรรทัดที่มี comment
// Error!เขียนไว้ให้เห็นว่า TypeScript จะฟ้อง — ถ้าคัดลอกทั้งบล็อกไปรันจริงด้วยtscต้อง ลบบรรทัดนั้นทิ้งก่อน ไม่งั้นทั้งไฟล์จะ compile ไม่ผ่าน
💡 ทำไมต้อง
as const? — ถ้าไม่ใส่ TypeScript จะ widen"/"เป็นstringทั่วไป →RoutePathก็เป็นstring(เสียประโยชน์ของ literal) —as constล็อกให้เป็น literal เป๊ะ ๆ (จากบทที่ 02)
Part 10: Checkpoint
1. Partial<T> ทำอะไร ใช้ตอนไหน?
ทำให้ทุก field ของ T เป็น optional (?) — ใช้กับฟังก์ชันอัปเดตข้อมูลบางส่วน ที่คนเรียกอาจส่งมาแค่บาง field
2. Pick กับ Omit ต่างกันยังไง?
Pick<T, K> หยิบเฉพาะ field ที่ระบุ Omit<T, K> ตัด field ที่ระบุออกเหลือที่เหลือ — เลือก Pick ถ้าอยากเก็บไม่กี่ field, เลือก Omit ถ้าอยากตัดไม่กี่ field
3. Record<K, V> คืออะไร?
สร้าง object type โดยระบุว่า key เป็นแบบ K และ value เป็นแบบ V — เช่น Record<string, number> = object ที่ key เป็น string, value เป็น number
4. Exclude กับ Extract ต่างกันยังไง?
Exclude<T, U> เอาสมาชิกของ union T ที่ตรงกับ U ออก Extract<T, U> เก็บเฉพาะ สมาชิกที่ตรง — เป็นคู่ตรงข้ามกัน
5. ReturnType กับ Parameters ใช้ทำอะไร?
ReturnType<typeof fn> ดึง type ของค่าที่ฟังก์ชันคืน Parameters<typeof fn> ดึง type ของพารามิเตอร์ทั้งหมดเป็น tuple — ใช้นำ type ของฟังก์ชันที่มีอยู่มาใช้ซ้ำ
6. typeof ในตำแหน่ง type ทำอะไร ต่างจาก typeof ตอน runtime ยังไง?
typeof ในตำแหน่ง type ดึง type ของค่าที่มีอยู่ออกมา ส่วน typeof ในโค้ดปกติ (เช่นใน if) เช็กชนิดค่าตอน runtime — เขียนเหมือนกันแต่คนละเรื่อง ดูจากตำแหน่งที่ใช้
7. ทำไม DeepPartial ต้อง recursive?
เพราะ object ซ้อนกันได้ไม่จำกัดชั้น Partial ปกติทำแค่ชั้นบนสุด — DeepPartial ต้องเรียกตัวเองเพื่อ "ดำดิ่ง" ลงไปทำทุกชั้น
Part 11: สรุปบทนี้
- Utility type = generic สำเร็จรูปที่ TypeScript เตรียมไว้ — แปลง type ที่มีอยู่เป็น type ใหม่
- แปลง object:
Partial(ทุก field optional),Required,Readonly,Pick(เลือก),Omit(ตัด) Record<K, V>— สร้าง object type จาก key และ value- ทำงานกับ union:
Exclude,Extract,NonNullable - ทำงานกับฟังก์ชัน:
ReturnType,Parameters,Awaited - เครื่องมือพื้นฐาน:
keyof,T[K],typeof(ในตำแหน่ง type),keyof typeof - utility ทั้งหมดสร้างจาก mapped type — สร้างเองได้ (ลงลึกบทที่ 10)
- Zod +
z.infer— เขียน schema ที่เดียว ได้ทั้งการตรวจสอบและ type
บทถัดไป — Type Narrowing