โหมดมืด
บทที่ 10 — Advanced Patterns (Type-Level Programming)
⏱ ใช้เวลา 3–4 ชั่วโมง — บทนี้ยากที่สุดในเล่ม
อ่านก่อนเริ่ม — ตั้งความคาดหวังให้ถูก
🚩 โซนขั้นสูง — ทั้งบทนี้ข้ามได้ บทนี้ยากที่สุดในเล่ม และ ไม่จำเป็นต่อการเขียน TypeScript ในงานประจำวัน ถ้าคุณเพิ่งเริ่ม ข้ามทั้งบทไปบทที่ 11 ได้เลย แล้วค่อยกลับมาเมื่อเจอสถานการณ์จริงที่ต้องใช้ — การข้ามบทนี้ไม่ทำให้คุณ "เขียน TypeScript ไม่เป็น" แต่อย่างใด
บทนี้คือ "type-level programming" (โปรแกรมระดับ type — เขียนตรรกะที่ทำงานในระดับ type system (ระบบ type) ไม่ใช่ที่ runtime) — การเขียนโปรแกรมในระดับ type ของ TypeScript
📚 คำศัพท์ที่จะเจอในบทนี้ (สรุปสั้น ๆ ไว้ก่อน — เจอจริงค่อยลงลึก)
- conditional type (type แบบมีเงื่อนไข) —
T extends U ? X : Yเลือก type ตามเงื่อนไข- mapped type (type ที่วน map) — "วน" ทุก key ของ type เดิมแล้วแปลง
- distributive conditional (กระจายตัวบน union) — conditional type "แตก" union ออกทำทีละตัว
- template literal type (แม่แบบข้อความ) — สร้าง string type จากแม่แบบ
`Hello ${...}`- recursive type (type ที่อ้างถึงตัวเอง) — type ที่เรียกตัวเองได้ เช่น JSON
inferkeyword (คำว่าinfer= "อนุมาน/เดา") — บอก TypeScript ให้ "ดึง type ส่วนหนึ่งออกมา"- variance marker (
out/in= ตัวกำกับทิศทาง) — บอกว่า generic เป็น covariant/contravariant- branded type (type ติดตรา) — เพิ่ม "ตรา" ลับให้ TypeScript แยก type ที่หน้าตาเหมือนกันออกได้
ถ้าอ่านแล้วยังไม่เข้าใจตอนนี้ ไม่เป็นไร — เอามาเทียบตอนเจอจริงในเนื้อหา
ขอบอกตรง ๆ ก่อน: เนื้อหาบทนี้ คุณไม่จำเป็นต้องใช้ทุกวัน แม้แต่มืออาชีพหลายคนก็เขียน type ระดับนี้ไม่บ่อย เทคนิคในบทนี้มีไว้สำหรับ:
- คนเขียน library ที่อยากให้ type ของ library ฉลาดและปลอดภัยสุด ๆ
- คนที่อยาก "อ่านโค้ด type ซับซ้อน" ของ library ดัง ๆ ออก
- คนที่อยากเข้าใจ TypeScript ถึงแก่นจริง ๆ
ดังนั้น — ถ้าอ่านบทนี้แล้วงง ไม่ใช่ความผิดของคุณ คุณยังเขียน TypeScript ได้ดีโดยไม่ต้องเชี่ยวชาญทุกอย่างในบทนี้ แนะนำให้:
- อ่านรอบแรกแบบ "เข้าใจแนวคิด" ไม่ต้องจำทุกบรรทัด
- กลับมาอ่านลึกเมื่อเจอสถานการณ์จริงที่ต้องใช้
เอาล่ะ มาเริ่มกัน
Part 1: Mapped Types เจาะลึก
เราเจอ mapped type คร่าว ๆ ในบทที่ 06 แล้ว — "การวนทำกับทุก key ของ object type" บทนี้จะลงลึก
1.1 ทบทวน
ทบทวน "mapped type" — เทคนิคสร้าง type ใหม่โดย "วน" ทุก key ของ type เดิม ([K in keyof T]) แล้วแปลงแต่ละ field เหมือนการ map array แต่ทำในระดับ type:
typescript
type Stringify<T> = {
[K in keyof T]: string;
// ↑ "สำหรับทุก key K ใน T" — ลูปในระดับ type
};1.2 Modifier + และ - — เพิ่ม/ลบตัวกำกับ
ใน mapped type เราสามารถ "เพิ่ม" หรือ "ลบ" ตัวกำกับ readonly และ ? ได้ ด้วยเครื่องหมาย + / -
typescript
// ลบ ? ออก (ทำให้ทุก field บังคับ) — นี่คือวิธีที่ TypeScript สร้าง Required
type MyRequired<T> = {
[K in keyof T]-?: T[K];
// ↑ -? = "ลบเครื่องหมาย ? ออก"
};
// ลบ readonly ออก (ทำให้แก้ได้)
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
// ↑ -readonly = "ลบ readonly ออก"
};
// เพิ่ม readonly (เหมือน Readonly ปกติ — + ใส่หรือไม่ใส่ก็ได้)
type MyReadonly<T> = {
+readonly [K in keyof T]: T[K];
};นี่คือเบื้องหลังของ utility type ที่เราใช้ — Required ใช้ -?, Readonly ใช้ +readonly ฯลฯ
1.3 Key Remapping (การเปลี่ยนชื่อ key) ด้วย as — เปลี่ยนชื่อ key
ความสามารถที่ทรงพลังของ mapped type — เราเปลี่ยน "ชื่อ" ของ key ได้ระหว่างวนลูป ด้วยคำว่า as:
typescript
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
// ↑ as ... = เปลี่ยนชื่อ key ใหม่
// ↑ Capitalize<S> คือ built-in utility type ของ TypeScript (ไม่ต้อง import)
// แปลงตัวอักษรแรกให้เป็นพิมพ์ใหญ่ เช่น Capitalize<"name"> = "Name"
};
type User = { name: string; age: number };
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getAge: () => number;
// }อธิบาย: ระหว่างวนแต่ละ key K — as \get${Capitalize<K & string>}`สั่งให้ "เปลี่ยนชื่อ key เป็น get + ตัวพิมพ์ใหญ่หน้า" ผลคือname→getName`
เรื่อง K & string: ปกติ key ของ object ใน TypeScript ไม่ได้เป็น string เสมอไป — อาจเป็น number หรือ symbol ก็ได้ (ทบทวนจากบทที่ 05/06 เรื่อง keyof) แต่ template literal type (`get${...}`) ใช้ได้กับ string เท่านั้น เราจึงต้อง K & string เพื่อ "บังคับ" ให้ TypeScript มองว่า K เป็น string เสมอในจุดนี้ — เป็นการ narrow type ให้แคบลงพอจะใช้กับ template literal ได้
1.4 กรอง key ออก ด้วย as ... never
เคล็ดลับ: ถ้าระหว่าง remap เราเปลี่ยนชื่อ key เป็น never — key นั้นจะ "หายไป" จากผลลัพธ์ ใช้เทคนิคนี้ "กรอง" field:
typescript
// เก็บเฉพาะ field ที่ type เป็น string
type StringFieldsOnly<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
// ↑ ถ้า field เป็น string → คงชื่อ K ไว้
// ถ้าไม่ใช่ → เปลี่ยนชื่อเป็น never → field หายไป
};
type Mixed = { name: string; age: number; email: string; active: boolean };
type OnlyStrings = StringFieldsOnly<Mixed>;
// { name: string; email: string } ← เหลือแค่ field ที่เป็น stringPart 2: แม่แบบข้อความ — Template Literal Types
2.1 แนวคิด
จำ literal type จากบทที่ 02 ได้ไหม — type ที่เป็นค่าเป๊ะ ๆ เช่น "hello" Template literal type คือการสร้าง string literal type โดยใช้ "แม่แบบ" — เหมือน template string (`${...}`) แต่ในระดับ type:
typescript
type Greeting = `Hello, ${string}`;
const a: Greeting = "Hello, Alice"; // ✅ ตรงแม่แบบ
const b: Greeting = "Hello, World"; // ✅
const c: Greeting = "Hi, Alice"; // ❌ Error! ไม่ขึ้นต้นด้วย "Hello, "`Hello, ${string}` หมายถึง "string ใด ๆ ที่ขึ้นต้นด้วย Hello, แล้วตามด้วยอะไรก็ได้"
2.2 พลังที่แท้จริง — รวมร่างกับ union
เมื่อใช้ template literal กับ union มันจะ "สร้างทุกการจับคู่" ให้:
typescript
type Color = "red" | "green" | "blue";
type Shade = "light" | "dark";
type ColorVariant = `${Shade}-${Color}`;
// "light-red" | "light-green" | "light-blue" |
// "dark-red" | "dark-green" | "dark-blue"
// — สร้างครบทั้ง 6 แบบให้อัตโนมัติ!ตัวอย่างจริง — สร้าง type ของ CSS class:
typescript
type Direction = "top" | "right" | "bottom" | "left";
type MarginClass = `margin-${Direction}`;
// "margin-top" | "margin-right" | "margin-bottom" | "margin-left"2.3 ใช้ร่วมกับ infer — แยกส่วนของ string
template literal type ใช้กับ infer (จากบทที่ 05) เพื่อ "แยกชิ้นส่วน" ของ string ได้:
typescript
// ดึง "method" ออกจาก string แบบ "GET /users"
type ExtractMethod<T> = T extends `${infer Method} ${string}` ? Method : never;
type A = ExtractMethod<"GET /users">; // "GET"
type B = ExtractMethod<"POST /products">; // "POST"`${infer Method} ${string}` แปลว่า "string ที่มีรูปแบบ (อะไรสักอย่าง)(ช่องว่าง)(อะไรสักอย่าง) — ให้ดึงส่วนแรกมาเรียก Method"
Part 3: Conditional Types เจาะลึก
เราเจอ conditional type (T extends U ? X : Y) ในบทที่ 05 แล้ว บทนี้ดูพฤติกรรมขั้นสูง
3.1 พฤติกรรมกระจายตัวบน union (Distributive)
นี่คือพฤติกรรมที่ทำให้มือใหม่งงมาก — เมื่อ conditional type เจอ generic ที่เป็น union มันจะ "แตก union ออกทำทีละตัว" แล้วรวมผลกลับ:
typescript
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// TypeScript ไม่ได้ทำกับ (string | number) ทั้งก้อน
// แต่แตกออก: ToArray<string> | ToArray<number>
// = string[] | number[]ผลลัพธ์เป็น string[] | number[] ไม่ใช่ (string | number)[] — เพราะการ "กระจายตัว"
ถ้าไม่อยากให้กระจายตัว — ครอบ generic ด้วย tuple [T]:
typescript
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;
type Result = ToArrayNoDistribute<string | number>;
// = (string | number)[] ← ทำกับทั้งก้อน ไม่แตก3.2 ใช้ Distributive กรอง union
พฤติกรรม distributive มีประโยชน์ — ใช้ "กรอง" สมาชิกของ union:
typescript
type KeepStrings<T> = T extends string ? T : never;
type Result = KeepStrings<"a" | 42 | "b" | true>;
// แตกทำทีละตัว:
// "a" extends string ? → "a"
// 42 extends string ? → never (หายไป)
// "b" extends string ? → "b"
// true extends string? → never (หายไป)
// = "a" | "b"(นี่คือเบื้องหลังของ Extract utility type)
3.3 Recursive Conditional Type — type ที่เรียกตัวเอง
conditional type เรียกตัวเองได้ ใช้แก้ปัญหาที่ "ลึกไม่จำกัดชั้น":
typescript
// แกะ array ที่ซ้อนกันออกจนได้ type ของสมาชิกข้างในสุด
type DeepFlattenArray<T> = T extends Array<infer Inner>
? DeepFlattenArray<Inner> // ★ ถ้ายังเป็น array อยู่ → เรียกตัวเองอีก
: T; // ถ้าไม่ใช่ array แล้ว → คืนค่า
type A = DeepFlattenArray<number[][][]>; // number
type B = DeepFlattenArray<string[]>; // stringPart 4: Branded Types (type ติดตรา) — แก้ปัญหา Structural Typing
เราสัญญาไว้ในบทที่ 04 ว่าจะอธิบายเรื่องนี้เต็ม ๆ — ถึงเวลาแล้ว
4.1 ทบทวนปัญหา
จำ structural typing จากบทที่ 00 ได้ไหม — TypeScript ดู "หน้าตา" ไม่ดู "ชื่อ" ปัญหาคือ:
typescript
type UserId = string;
type PostId = string;
function getUser(id: UserId) { /* ... */ }
const postId: PostId = "post-123";
getUser(postId); // 😱 ไม่ error! เพราะ UserId กับ PostId ก็คือ string เหมือนกันเราเผลอเอา PostId ไปใช้กับฟังก์ชันที่ต้องการ UserId — TypeScript ไม่จับ เพราะหน้าตา (string) เหมือนกัน นี่เป็นบั๊กที่อันตรายมาก (ลองนึกภาพในระบบจริง — เอา PostId ไปเรียก getUser() ดึงผิดคน เผยข้อมูลผิด account!)
4.2 ทางออก — "ติดตรา" (Brand) ให้ type
แนวคิด: เราจะ "ปลอม" ให้ TypeScript คิดว่า UserId กับ PostId มีหน้าตาต่างกัน — โดยเพิ่ม property พิเศษที่ มีอยู่แค่ในระดับ type (ไม่มีจริงตอน runtime):
typescript
// แบบที่ 1 — literal-key brand (เห็นบ่อย เข้าใจง่าย)
type UserId = string & { readonly __brand: "UserId" };
type PostId = string & { readonly __brand: "PostId" };string & { readonly __brand: "UserId" } แปลว่า "เป็น string และ มี property __brand ที่เป็น "UserId"" — ตอนนี้ UserId กับ PostId มี __brand ต่างกัน (literal "UserId" ≠ literal "PostId") TypeScript จึงแยกแยะได้ — getUser(somePostId) จะ error
⚠️ ข้อจำกัดของ literal-key brand: pattern
string & {...}ไม่ใช่ "nominal brand เต็มตัว" — เช่น ส่งค่าที่มี brand (เช่นUserId) ไปให้พารามิเตอร์ที่รับแค่stringธรรมดา ก็ยังทำได้อยู่ดี (ซึ่งมักเป็นพฤติกรรมที่เราต้องการอยู่แล้ว เพราะUserIdก็คือ string ชนิดหนึ่ง) แต่ถ้าอยากได้การป้องกันที่แน่นกว่านี้ — กันไม่ให้ผสมกันได้เลยแม้แต่ทิศทางเดียว — ให้ใช้unique symbolแบบที่ 2 ด้านล่าง
💡 แบบที่ 2 —
unique symbolbrand (แนะนำสำหรับ library/production)ปัญหาของ
__brandแบบ literal key: ถ้าสอง library บังเอิญตั้งชื่อ key__brandเหมือนกัน อาจ collide (collide = ชนกัน — สอง type ที่ตั้งใจให้คนละตัว กลับถูกมองเป็นตัวเดียวกัน) โดยไม่ตั้งใจ —unique symbolทำให้ key เป็นของเรา 100% (ไม่มี library ไหนชนกันได้):typescriptdeclare const userIdBrand: unique symbol; declare const productIdBrand: unique symbol; type UserId = string & { readonly [userIdBrand]: true }; type ProductId = string & { readonly [productIdBrand]: true };ในบทเรียนนี้เราใช้แบบ literal key เพื่อให้อ่านง่าย — แต่ถ้าคุณเขียน library ที่จะ publish ขึ้น npm หรืออยู่ใน monorepo ใหญ่ ให้ใช้
unique symbolแทน
4.3 ใช้งาน
เนื่องจาก __brand ไม่มีอยู่จริง เราสร้าง branded value ด้วย as ผ่านฟังก์ชันที่ตรวจสอบ:
typescript
type UserId = string & { readonly __brand: "UserId" };
// ฟังก์ชันสร้าง UserId — ตรวจสอบก่อน แล้วค่อย "ติดตรา"
function makeUserId(value: string): UserId {
if (!value.startsWith("user-")) {
throw new Error("รูปแบบ UserId ไม่ถูกต้อง");
}
return value as UserId; // ติดตรา
}
function getUser(id: UserId) { /* ... */ }
const id = makeUserId("user-123");
getUser(id); // ✅
getUser("user-123"); // ❌ Error! string ธรรมดาไม่ใช่ UserId (ไม่มีตรา)ตอนนี้ — string ธรรมดาส่งให้ getUser ไม่ได้แล้ว ต้องผ่าน makeUserId ที่ตรวจสอบเท่านั้น ปลอดภัยขึ้นมาก
💡 ข้อสำคัญ:
__brandไม่มีอยู่จริงตอน runtime — มันเป็นแค่ "ตราในจินตนาการ" ที่ TypeScript ใช้แยกแยะ ตอน runtimeUserIdก็คือ string ธรรมดา ไม่มี overhead อะไร
Part 5: Recursive Types — type ที่อ้างถึงตัวเอง
บางโครงสร้างข้อมูล "ซ้อนตัวเองได้ไม่จำกัด" — เช่น JSON ตัวอย่างคลาสสิก: type ที่อธิบาย "ค่า JSON ใด ๆ":
typescript
type JsonValue =
| string
| number
| boolean
| null
| JsonValue[] // ★ array ของ JsonValue (อ้างถึงตัวเอง)
| { [key: string]: JsonValue }; // ★ object ที่ value เป็น JsonValue (อ้างถึงตัวเอง)
const data: JsonValue = {
name: "Alice",
age: 30,
active: true,
tags: ["admin", "user"],
address: {
city: "Bangkok",
coordinates: [13.7, 100.5],
},
};type JsonValue อ้างถึงตัวเอง — ทำให้มันอธิบายโครงสร้างที่ซ้อนลึกเท่าไรก็ได้
⚠️ ข้อจำกัด (อ่านผ่าน ๆ ก็พอ): TypeScript มี เพดานความลึก ที่ตั้งไว้ในตัว compiler — ถ้า recursive ลึกเกินไป (เช่น JSON ที่ซ้อนหลายสิบชั้น หรือ type-level program ที่คำนวณซ้ำมาก) จะแจ้ง error
Type instantiation is excessively deep and possibly infiniteแปลว่า "type ลึกเกินไป อาจไม่จบ" ในทางปฏิบัติเจอเฉพาะตอนเขียน type-level program จัด ๆ — JSON ปกติของ API ไม่เจอสำหรับคนที่อยากรู้เพิ่ม — เรื่อง compiler internals
เพดานความลึกมีหลายตัว เช่น instantiation depth (ความลึกของการสร้าง instance type), type comparison depth (ความลึกของการเทียบ type), และ tail-call optimization (TCO — เทคนิคช่วยให้ฟังก์ชัน/type ที่เรียกตัวเองซ้ำ ๆ ไม่ต้องกินหน่วยความจำเพิ่มทุกครั้งที่เรียก) สำหรับ recursive conditional type ตัวเลขเป๊ะ ๆ จะเปลี่ยนตามเวอร์ชัน TS — ดู release notes หรือซอร์สโค้ดของ compiler ถ้าต้องการความแม่นยำ
Part 6: Tuple Manipulation — จัดการ tuple ระดับ type
เราจัดการ tuple ในระดับ type ได้ ด้วย infer และ spread ...:
typescript
// ดึงสมาชิกตัวแรก
type Head<T extends any[]> = T extends [infer First, ...any[]] ? First : never;
// ดึงสมาชิกตัวสุดท้าย
type Tail<T extends any[]> = T extends [...any[], infer Last] ? Last : never;
// นับจำนวนสมาชิก
type Count<T extends any[]> = T["length"];
// เพิ่มสมาชิกท้าย
type Append<T extends any[], V> = [...T, V];
type A = Head<[1, 2, 3]>; // 1
type B = Tail<[1, 2, 3]>; // 3
type C = Count<[1, 2, 3]>; // 3
type D = Append<[1, 2], 3>; // [1, 2, 3]เทคนิคพวกนี้ใช้ใน library ขั้นสูง (เช่นการทำ type ของฟังก์ชัน curry — เทคนิคแปลงฟังก์ชันหลายพารามิเตอร์ให้เป็นฟังก์ชันทีละตัว) — งานทั่วไปไม่ค่อยได้ใช้ตรง ๆ
⚠️ หมายเหตุ: ตัวอย่าง
Appendข้างบนได้ผลเป๊ะเพราะTที่ส่งเข้าไปเป็น tuple literal (เช่น[1, 2]ที่รู้จำนวนและลำดับสมาชิกแน่นอน) ถ้าTเป็น array กว้าง ๆ แทน (เช่นnumber[]ที่ไม่รู้ความยาวแน่นอน) ผลลัพธ์จะไม่ได้ fixed-length tuple ตามที่หวัง — จะกลายเป็น[...number[], V]แทน pattern นี้จึงใช้ได้แม่นยำเฉพาะตอนTเป็น tuple literal จริง ๆ
💡 หมายเหตุเรื่อง
any[]: ตัวอย่างข้างบนใช้T extends any[]ตามแบบที่เห็นบ่อยในซอร์สของ library — แต่ในโปรเจกต์ที่เปิด lint เข้มany[]อาจถูกฟ้องunknown[]เข้มงวดกว่าและปลอดภัยกว่า แต่การสลับไปใช้ไม่ใช่แค่เปลี่ยนคำเฉย ๆ — ต้องปรับโค้ดในบางจุดตามมาด้วย (เช่นค่าที่ดึงออกมาจะมี type เป็นunknownแทนanyทำให้ต้อง narrow ก่อนใช้งานจริง) บางสไตล์ไกด์ก็แนะนำreadonly unknown[]ด้วย
Part 7: as const satisfies — รวมพลังสองคีย์เวิร์ด
จำ as const (บทที่ 02) และ satisfies (บทที่ 04) ได้ไหม — เราใช้ทั้งคู่พร้อมกันได้ เพื่อให้ได้ผลดีที่สุด:
typescript
type RouteConfig = Record<string, string>;
const routes = {
home: "/",
profile: "/profile",
settings: "/settings",
} as const satisfies RouteConfig;
// ↑ as const = แช่แข็งให้เป็น literal เป๊ะ ๆ + readonly
// ↑ satisfies = ตรวจว่าตรงกับ RouteConfig
// ผลลัพธ์ — ได้ทั้งสองอย่าง:
type RouteName = keyof typeof routes;
// "home" | "profile" | "settings" ← เป๊ะ เพราะ as const
routes.home; // type: "/" ← เป๊ะ เพราะ as const
// และถ้า value ไม่ตรง RouteConfig → satisfies จะฟ้องas const satisfies คือ pattern ที่ดีที่สุดสำหรับ "object คงที่ที่อยากได้ทั้งความเป๊ะและการตรวจสอบ" — เช่น config, ตาราง lookup, ชุดค่าคงที่
Part 8: เรื่องประสิทธิภาพ (Performance)
type ที่ซับซ้อนเกินไปทำให้ TypeScript ทำงานช้า (autocomplete หน่วง, compile นาน) ข้อควรระวัง:
- อย่า recursive ลึกเกินจำเป็น — type ที่เรียกตัวเองหลายสิบชั้นทำให้ช้า
skipLibCheck: true(เราเปิดไว้แล้วในบทที่ 01) — ข้ามการตรวจ type ใน library ช่วยให้เร็วขึ้น- อย่าใช้ type-level computation เกินงาม — เทคนิคในบทนี้เท่ ๆ แต่ถ้าใช้พร่ำเพรื่อ โค้ดจะอ่านยากและ IDE ช้า
🎯 หลักคิด: type ที่ดีคือ type ที่ "ช่วยคนอ่านเข้าใจ" ไม่ใช่ type ที่ "โชว์ว่าเราเก่ง" — ถ้า type ซับซ้อนจนเพื่อนร่วมทีมอ่านไม่ออก มันอาจซับซ้อนเกินไป
Part 9: Lab — ลงมือทำ
Lab 1 — Branded type
ฝึกเทคนิค "branded type" — ทำให้ string ธรรมดากับ Email เป็นคนละ type (แม้ค่าจริงเป็น string) โดยแปะ marker ลับ ป้องกันการส่ง string ที่ยังไม่ validate เข้าฟังก์ชันที่ต้องการ Email:
typescript
type Email = string & { readonly __brand: "Email" };
function makeEmail(value: string): Email {
if (!/^[^@]+@[^@]+\.[^@]+$/.test(value)) {
throw new Error("รูปแบบอีเมลไม่ถูกต้อง");
}
return value as Email;
}
function sendNewsletter(to: Email): void {
console.log(`ส่งจดหมายถึง ${to}`);
}
const email = makeEmail("alice@example.com");
sendNewsletter(email); // ✅
sendNewsletter("not-an-email"); // ❌ Error! ไม่ใช่ EmailLab 2 — Mapped type กรอง field
ฝึกใช้ mapped type + key remapping (as ... extends ... ? K : never) เพื่อกรองเอาเฉพาะ field บางชนิด (เช่น field ที่เป็น method) ออกมาเป็น type ใหม่ — เห็นพลังของ type-level programming:
typescript
// สร้าง type ที่เก็บเฉพาะ field ที่เป็นฟังก์ชัน
// หมายเหตุ: ใช้ (...args: any[]) => any แทน Function เพราะ kฎ default ของ typescript-eslint
// (linter — เครื่องมือตรวจคุณภาพโค้ด) ชื่อ rule @typescript-eslint/no-unsafe-function-type
// ห้ามใช้ bare Function (Function ตัวเปล่า ไม่มีลายเซ็น) — มันกว้างเกินไป
// (รวมถึง class constructors และของแปลก ๆ อื่น)
type MethodsOnly<T> = {
[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
};
type Widget = {
name: string;
width: number;
render: () => void;
destroy: () => void;
};
type WidgetMethods = MethodsOnly<Widget>;
// { render: () => void; destroy: () => void }Lab 3 — Template literal type
typescript
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Resource = "users" | "products";
type ApiRoute = `${HttpMethod} /${Resource}`;
// "GET /users" | "GET /products" | "POST /users" | ... (8 แบบ)
function callApi(route: ApiRoute): void {
console.log(`เรียก ${route}`);
}
callApi("GET /users"); // ✅
callApi("PATCH /users"); // ❌ Error! ไม่มี method PATCH
callApi("GET /orders"); // ❌ Error! ไม่มี resource ordersPart 10: Checkpoint
1. Modifier + และ - ใน mapped type ทำอะไร?
ใช้เพิ่ม/ลบตัวกำกับ readonly และ ? เช่น -? ลบ optional ออก (ทำให้บังคับ), -readonly ลบ readonly ออก (ทำให้แก้ได้) — เป็นเบื้องหลังของ Required, Mutable
2. Key remapping ด้วย as ทำอะไร?
เปลี่ยนชื่อ key ระหว่างวน mapped type — และถ้า remap เป็น never key นั้นจะหายไป ใช้กรอง field ได้
3. Template literal type คืออะไร?
การสร้าง string literal type จาก "แม่แบบ" เช่น `Hello, ${string}` — เมื่อใช้กับ union จะสร้างทุกการจับคู่ให้อัตโนมัติ
4. พฤติกรรม distributive ของ conditional type คืออะไร?
เมื่อ conditional type เจอ generic ที่เป็น union มันจะแตก union ออกทำทีละตัวแล้วรวมผล — ป้องกันได้ด้วยการครอบ [T]
5. Branded type แก้ปัญหาอะไร?
แก้ปัญหา structural typing — ทำให้ type ที่หน้าตาเหมือนกัน (เช่น UserId กับ ProductId ที่เป็น string ทั้งคู่) ถูก TypeScript แยกแยะได้ โดยเพิ่ม property __brand ที่มีแค่ระดับ type
6. Recursive type มีข้อจำกัดอะไร?
TypeScript มีขีดจำกัดความลึก (เลขเป๊ะ ๆ ขึ้นกับเวอร์ชัน — ในการคำนวณซับซ้อนมักอยู่ราวหลักสิบชั้น) — ถ้า recursive ลึกเกินไปจะ error งานทั่วไปแทบไม่เจอ
7. as const satisfies ใช้ตอนไหน?
ใช้กับ object คงที่ที่ต้องการทั้ง "ความเป๊ะของ literal type" (จาก as const) และ "การตรวจสอบว่าตรง type" (จาก satisfies) — เหมาะกับ config, lookup table
Part 11: สรุปบทนี้
- Mapped type —
+/-ปรับ modifier,asเปลี่ยน/กรอง key - Template literal type — สร้าง string type จากแม่แบบ, รวมกับ union ได้ทุกการจับคู่
- Conditional type — distributive (กระจายบน union), recursive (เรียกตัวเอง)
- Branded type — แก้ปัญหา structural typing แยกแยะ type ที่หน้าตาเหมือนกัน
- Recursive type — อธิบายโครงสร้างซ้อนได้ (เช่น JSON) มีขีดจำกัดความลึก
as const satisfies— pattern ที่ดีที่สุดสำหรับ object คงที่- ⚠️ type ที่ดี = type ที่ช่วยคนอ่านเข้าใจ ไม่ใช่ type ที่ซับซ้อนเพื่อโชว์
บทถัดไป (บทสุดท้าย) — เอา TypeScript ไปใช้งานจริง
← บทที่ 09 | สารบัญ | บทที่ 11: TS in Practice →
🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-03