Foundations & Mental Model
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”TypeScript คือ JavaScript บวกกับ static type system ที่ทำงานตอน compile time และหายไปตอน runtime ไม่เปลี่ยนสิ่งที่ code ของคุณ ทำ — แต่เปลี่ยนสิ่งที่ compiler ยอมให้คุณเขียน จับ bug ทั้งกลุ่มได้ก่อนที่ code จะรันเสียอีก
ทุกอย่างขั้นสูงในคอร์สนี้ — generics, conditional types, mapped types — คือ system เดียวกันนี้ที่ถูกต่อยอดไปจนสุดทาง เข้าใจ mental model ให้ถูก แล้วที่เหลือจะตามมาเอง
โมดูลนี้ครอบคลุมอะไรบ้าง
หัวข้อที่มีชื่อว่า “โมดูลนี้ครอบคลุมอะไรบ้าง”| บทเรียน | สิ่งที่คุณจะได้เรียน |
|---|---|
| Why TypeScript | type ให้อะไรกับเรา และเส้นแบ่ง compile-time / runtime |
| The type-system mindset | type คือ เซตของ value — mental model ที่มีประโยชน์ที่สุด |
| Structural typing | ทำไม TypeScript สนใจ shape ไม่ใช่ชื่อ |
| Type inference | compiler เดา type ที่คุณไม่ได้เขียนออกมาได้อย่างไร |
เลนส์สำหรับทั้งคอร์ส
หัวข้อที่มีชื่อว่า “เลนส์สำหรับทั้งคอร์ส”สองไอเดียนี้ พอเอามารวมกัน อธิบายพฤติกรรมของ TypeScript ได้แทบทุกอย่างที่คุณจะเจอ:
flowchart TB sets["Type คือ เซต ของ value (number = ทุกจำนวน)"] --> assign["Assignability = ความสัมพันธ์แบบ subset"] struct["Typing เป็นแบบ STRUCTURAL (ดูที่ shape ไม่ใช่ชื่อ)"] --> compat["Compatibility = shape เข้ากันได้ไหม"] assign --> ts["ทุกพฤติกรรมของ TS"] compat --> ts
เวลาที่การ assign ผ่านได้ เป็นเพราะเซตของ value ของ type หนึ่งพอดีอยู่ในอีก type หนึ่ง เวลาที่ object สองตัวใช้แทนกันได้ เป็นเพราะ shape ของตัวเองตรงกัน จำสองไอเดียนี้ไว้ แล้ว TypeScript จะเลิกทำให้คุณประหลาดใจ
หมายเหตุเรื่องเวอร์ชัน ทุกอย่างในคอร์สนี้อิง TypeScript 7 — native Go compiler ที่ออกเมื่อกรกฎาคม 2026 และ strict เปิดเป็นค่าเริ่มต้น feature ที่เพิ่งมาใหม่ บทเรียนจะระบุเวอร์ชันที่แนะนำมันเข้ามา (เช่น “inferred type predicate, 5.5”) จะได้รู้ว่าอะไรเปลี่ยนไปจาก TypeScript ที่คุณอาจเคยเรียน
interface กับ type: การเลือก construct ครั้งแรก
หัวข้อที่มีชื่อว่า “interface กับ type: การเลือก construct ครั้งแรก”ไม่กี่นาทีหลังเริ่มเขียน TypeScript คุณจะต้องตั้งชื่อ object shape แรก และภาษานี้ให้สองทางเลือก: interface กับ type สำหรับ object shape ธรรมดา ทั้งคู่ทำงานแทบเหมือนกัน — แต่คำว่า “แทบ” ซ่อนความต่างจริงสองอย่างที่ควรรู้ไว้ตั้งแต่วันแรก
Declaration merging interface ประกาศซ้ำใน scope เดียวกันได้มากกว่าหนึ่งครั้ง — การประกาศจะ merge รวมกันเป็น interface เดียวที่มีทุก member จากทุกการประกาศ ส่วน type ทำแบบนั้นไม่ได้ — ประกาศซ้ำคือ error ทันที
interface Config { name: string;}interface Config { version: number;}
const c: Config = { name: "app", version: 1 }; // ✅ merged: both members required// @expect-error TS2300type Config = { name: string;};type Config = { version: number;};Merging ไม่ใช่เรื่องแปลกประหลาด — มันคือวิธีที่ .d.ts ใช้ต่อขยายของที่คุณไม่ได้เป็นเจ้าของ การเพิ่ม method ให้ array ทุกตัวทั่วทั้งโปรแกรม หน้าตาเหมือนตัวอย่างข้างบนเป๊ะ:
interface Array<T> { last(): T | undefined;}
const nums: number[] = [1, 2, 3];console.log(nums.last()); // ✅ Array<T> merged with lib.es5.d.ts's own Array<T>สิ่งนี้ใช้ได้เพราะ Array ใน TypeScript เองก็ประกาศด้วย interface library ที่อยากทำแบบเดียวกันด้วย type ทำไม่ได้เลย — นี่คือเหตุผลที่ library มักประกาศ public object contract ด้วย interface
Error message อ่านต่างกันเมื่อ shape ถูกประกอบขึ้นมา สำหรับ shape แบนราบตัวเดียว interface กับ type ให้ error หน้าตาเหมือนกัน ความต่างโผล่มาตอนคุณ ประกอบ shape จากอันอื่น — extends สำหรับ interface, & สำหรับ type alias:
Composing a shape changes what the error names
// @expect-error TS2345 TS2741interface Base { id: string }interface IUser extends Base { name: string }
type TBase = { id: string };type TUser = TBase & { name: string };
function greetI(u: IUser) {}function greetT(u: TUser) {}
greetI({ id: "1" }); // TS2741: missing 'name' in type 'IUser' — named by the interfacegreetT({ id: "1" }); // TS2345, with a nested line naming '{ name: string }' — the anonymous intersection member, not 'TBase' interface extends ยังคงเรียกชื่อ interface (IUser) ไม่ว่าจะต่อกันมากี่ชั้น ส่วน type intersection รายงาน assignability กับชื่อ alias ที่บรรทัดบนสุด แต่พอต้องอธิบายว่า ชิ้นไหน ที่พัง มันจะขยายออกเป็น anonymous object type ของ intersection member ตัวที่พัง แทนที่จะเรียกชื่อ TBase ไม่มีอันไหน “ผิด” — แต่ codebase ที่มี type alias ซ้อน intersection ลึก ๆ จะอ่าน error ได้รกกว่า interface extends แบบเดียวกัน
คอร์สนี้แนะนำให้ใช้อันไหนเมื่อไร เลือก interface สำหรับ object shape ที่คุณคาดว่าจะถูก extend, implement โดย class หรือถูก augment โดย code ของคนอื่น (พฤติกรรม merging คือจุดเด่นตรงนี้) เลือก type สำหรับทุกอย่างที่ interface แสดงไม่ได้เลย — union, tuple, mapped type, conditional type — และสำหรับ shape ที่ใช้ครั้งเดียวซึ่ง merging มีแต่จะเป็นกับดัก ทั้งคู่ถูกลบตอน compile เหมือนกัน — นี่คือการเลือกเรื่อง readability กับ API design ไม่ใช่เรื่อง performance
อ่าน error chain แบบ “is not assignable”
หัวข้อที่มีชื่อว่า “อ่าน error chain แบบ “is not assignable””มือใหม่มักอ่าน error หลายบรรทัดของ TypeScript จากบนลงล่างแล้วพยายามแก้บรรทัดแรก นั่นกลับทาง — บรรทัดแรกคือ บทสรุป ส่วนปัญหาจริงอยู่ล่างสุด
// @expect-error TS2322interface Address { city: string; zip: number }interface Person { name: string; address: Address }
const raw = { name: "Ada", address: { city: "NYC", zip: "10001" } };const p: Person = raw;error TS2322: Type '{ name: string; address: { city: string; zip: string; }; }' is not assignable to type 'Person'. The types of 'address.zip' are incompatible between these types. Type 'string' is not assignable to type 'number'.อ่านจาก ล่างขึ้นบน:
- บรรทัดล่างสุด — ความไม่ตรงกันตัวจริง
Type 'string' is not assignable to type 'number'.นี่คือข้อเท็จจริงเดียวที่จับต้องได้: มีจุดหนึ่งที่stringมาเจอกับnumberแล้วไม่พอดี - บรรทัดกลาง — ตำแหน่งที่แน่นอน
The types of 'address.zip' are incompatible...บอกชื่อ property path ตอนนี้คุณรู้แล้วว่า ตรงไหน:raw.address.zipไม่ใช่rawเอง - บรรทัดบนสุด — บทสรุปที่คนถาม
Type '{...}' is not assignable to type 'Person'.นี่คือสิ่งที่ทำให้เกิดการเช็คตั้งแต่แรก มัน restate object ทั้งก้อนที่คุณพยายาม assign แต่ไม่ได้บอกอะไรที่แก้ได้ตรง ๆ
การแก้อยู่ที่บรรทัดล่างสุดเสมอ: zip ต้องเป็น number (parse มันหรือแก้ที่จุดที่ผลิต "10001" เป็น string ตั้งแต่แรก) การไล่แก้บรรทัดบนแทน — cast raw เป็น Person หรือทำให้ Person.address หลวมเป็น any — เพียงแค่ทำให้ message เงียบ โดยไม่ได้แตะ bug จริงเลย
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Type error ไม่ใช่กำแพงข้อความให้กวาดตาผ่าน แต่คือคำอธิบายจุดเล็กสุดที่สอง shape ไม่ลงรอยกัน compiler บอกคุณเสมอ ที่ไหนสักแห่งใน message ว่า property ไหนกับสอง type ไหนที่ไม่ลงรอย — ทุกอย่างเหนือบรรทัดนั้นเป็นแค่วิธีที่ compiler ไล่มาเจอความไม่ลงรอย ไม่ใช่ตัวปัญหาเอง
flowchart BT bottom["Type string is not assignable to type number (the real mismatch)"] --> middle["at address.zip (the exact location)"] middle --> top["object not assignable to Person (the summary — not actionable on its own)"]
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | ข้อดี | ต้นทุน |
|---|---|---|
interface สำหรับ public shape | extend ได้, class implement ได้, และถูก merge/augment โดยไฟล์อื่นหรือ .d.ts ได้ | แสดง union, tuple หรือ shape ที่ไม่ใช่ object ไม่ได้ |
type สำหรับ public shape | แสดงได้ทุกอย่าง — union, tuple, mapped/conditional type — และไม่ถูกเปิดซ้ำแบบเงียบ ๆ ที่อื่น | Merge ไม่ได้: ประกาศชื่อซ้ำสองครั้งคือ error แข็ง ๆ ไม่ใช่ feature |
แก้บรรทัดบนของ error (cast, any) | เส้นแดงหายทันที | ไม่แตะความไม่ลงรอยจริง มักโผล่กลับมาเป็น runtime bug แทนที่จะเป็น compile error |
| แก้บรรทัดล่างของ error | แก้ปัญหาจริงที่ต้นตอ | ต้องอ่านผ่านบรรทัดสรุปไปก่อน ซึ่งช้ากว่าในช่วงแรก ๆ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”interfaceกับtypeใช้แทนกันได้เสมอ เลือกอันไหนก็ไม่ต่าง จริงสำหรับ shape แบนราบตัวเดียวที่ไม่เคยถูกเปิดซ้ำ แต่ผิดทันทีที่ merging, การประกอบด้วยextends/&, หรือ shape ที่ไม่ใช่ object เข้ามาเกี่ยวข้อง — แล้ว error message ก็ต่างกันไปแล้วด้วย- Error หลายบรรทัดแปลว่า bug ซับซ้อน ส่วนใหญ่แปลว่า bug ซ้อนลึก แต่ตัวมันเองง่ายมาก: property หนึ่งตัวมี type ผิด แล้ว compiler กำลังโชว์เส้นทางไปหามัน ไม่ใช่ความยากของการแก้
- บรรทัดบนสุดของ error คือสิ่งที่ต้องแก้ บรรทัดบนสุดมักเป็นส่วนที่ไม่เจาะจงที่สุดของ message เสมอ — เป็นบทสรุปของสิ่งที่คุณพยายามทำ ไม่ใช่การวินิจฉัย ข้อเท็จจริงที่เจาะจงอยู่ที่ล่างสุด
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ดที่ AI เขียนแล้วเจอ type error มักมีรูปแบบพลาดที่คาดเดาได้: มัน “แก้” บรรทัดนอกแทนที่จะแก้บรรทัดใน ตอน review diff ที่แก้ type error ให้เช็ค:
- การแก้เปลี่ยน type ของ leaf value จริง (
stringที่ควรจะเป็นnumber) หรือแค่ cast ทิ้งตัวแปรชั้นนอก (as Person,as unknown as Person) เพื่อให้ checker เงียบ? - ถ้าการแก้เป็น cast, error ที่เฉพาะเจาะจงกว่าเดิมยังคงเป็นจริงกับ data ตอน runtime ไหม? Cast ไม่เปลี่ยน value จริง ๆ ว่ามันคืออะไร
- Code ที่ “แก้แล้ว” ยังพังไหมถ้าคุณรัน mentally กับ shape จริง (ที่ไม่ถูก cast)?
🐛 หา bug ในโค้ดนี้
interface Address { city: string; zip: number }interface Person { name: string; address: Address }
const raw = { name: "Ada", address: { city: "NYC", zip: "10001" } };const p: Person = raw as unknown as Person; // "fixed" the red squiggleconsole.log(p.address.zip.toFixed(0));เฉลย
โค้ดนี้ compile ผ่านโดยไม่มี error เลย แล้วพังตอน runtime การ assign แบบไม่ cast เดิม (const p: Person = raw;) รายงานถูกต้องแล้วว่า address.zip เป็น string ไม่ใช่ number — นั่นคือข้อมูลจริงที่มีประโยชน์เกี่ยวกับ data raw as unknown as Person ไม่ได้เปลี่ยนว่า raw.address.zip คืออะไรจริง ๆ (ยังเป็น string "10001" เหมือนเดิม) มันแค่บอก compiler ให้เลิกเช็ค p.address.zip.toFixed(0) จึงเรียก method ของ number บน string แล้ว throw TypeError: p.address.zip.toFixed is not a function การแก้ที่ถูกต้องอยู่ที่บรรทัดล่างสุดของ error เดิม ไม่ใช่บรรทัดบน: แปลง zip code ให้เป็น number จริง ๆ ก่อนถึง raw หรือยอมรับ zip: string ใน Address ถ้า string คือสิ่งที่ data เป็นจริง ๆ