Skip to content

บทที่ 06 — Utility Types (type สำเร็จรูป)

← บทที่ 05 | สารบัญ | บทที่ 07: Type Narrowing →

⏱ ใช้เวลา 2–3 ชั่วโมง


บทนี้เรียนอะไร

ในบทที่ 05 เราเรียน generic — วิธีสร้าง type ที่ยืดหยุ่นด้วยตัวเอง บทนี้เราจะรู้จัก "generic สำเร็จรูป" ที่ TypeScript เตรียมไว้ให้แล้ว เรียกว่า Utility Types

ลองนึกภาพ — บ่อยครั้งเราอยาก "แปลง type ที่มีอยู่ ให้เป็น type ใหม่" เช่น:

  • มี type User อยากได้ version ที่ "ทุก field ไม่บังคับ" (สำหรับฟอร์มแก้ไข)
  • มี type User อยากได้ version ที่ "ตัด field password ออก" (สำหรับส่งให้ 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 "ต้องมี" (ตัด ? ทิ้งทั้งหมด)

ตรงข้ามกับ PartialRequired<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 กับ name

Pick<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]  ← เป็น tuple

Parameters คืน type ของพารามิเตอร์ทั้งหมดเป็น tuple

4.3 Awaited<T> — ดึง type ที่ Promise ห่อไว้

Awaited<T> ใช้เพื่อ "แกะ" type ที่ซ้อนอยู่ใน Promise ออกมา เหมือนรอผล await แล้วถามว่าค่าที่ได้มีชนิดอะไร:

typescript
type Result = Awaited<Promise<string>>;
// string  ← แกะ Promise ออก เหลือ string

Awaited "แกะ" 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 ของ property K ใน 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 เหล่านี้ออกก่อน เช่น:
typescript
type 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" });   // ✅ ส่งแค่ theme

Lab 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


← บทที่ 05 | สารบัญ | บทที่ 07: Type Narrowing →