ข้ามไปยังเนื้อหา

Foundations & Mental Model

TypeScript คือ JavaScript บวกกับ static type system ที่ทำงานตอน compile time และหายไปตอน runtime ไม่เปลี่ยนสิ่งที่ code ของคุณ ทำ — แต่เปลี่ยนสิ่งที่ compiler ยอมให้คุณเขียน จับ bug ทั้งกลุ่มได้ก่อนที่ code จะรันเสียอีก

ทุกอย่างขั้นสูงในคอร์สนี้ — generics, conditional types, mapped types — คือ system เดียวกันนี้ที่ถูกต่อยอดไปจนสุดทาง เข้าใจ mental model ให้ถูก แล้วที่เหลือจะตามมาเอง

บทเรียนสิ่งที่คุณจะได้เรียน
Why TypeScripttype ให้อะไรกับเรา และเส้นแบ่ง compile-time / runtime
The type-system mindsettype คือ เซตของ value — mental model ที่มีประโยชน์ที่สุด
Structural typingทำไม TypeScript สนใจ shape ไม่ใช่ชื่อ
Type inferencecompiler เดา 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 ที่คุณอาจเคยเรียน

ไม่กี่นาทีหลังเริ่มเขียน 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 TS2300
type 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 TS2741
interface 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 interface
greetT({ id: "1" }); // TS2345, with a nested line naming '{ name: string }' — the anonymous intersection member, not 'TBase'
Open in TS Playground

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 หลายบรรทัดของ TypeScript จากบนลงล่างแล้วพยายามแก้บรรทัดแรก นั่นกลับทาง — บรรทัดแรกคือ บทสรุป ส่วนปัญหาจริงอยู่ล่างสุด

// @expect-error TS2322
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;
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'.

อ่านจาก ล่างขึ้นบน:

  1. บรรทัดล่างสุด — ความไม่ตรงกันตัวจริง Type 'string' is not assignable to type 'number'. นี่คือข้อเท็จจริงเดียวที่จับต้องได้: มีจุดหนึ่งที่ string มาเจอกับ number แล้วไม่พอดี
  2. บรรทัดกลาง — ตำแหน่งที่แน่นอน The types of 'address.zip' are incompatible... บอกชื่อ property path ตอนนี้คุณรู้แล้วว่า ตรงไหน: raw.address.zip ไม่ใช่ raw เอง
  3. บรรทัดบนสุด — บทสรุปที่คนถาม Type '{...}' is not assignable to type 'Person'. นี่คือสิ่งที่ทำให้เกิดการเช็คตั้งแต่แรก มัน restate object ทั้งก้อนที่คุณพยายาม assign แต่ไม่ได้บอกอะไรที่แก้ได้ตรง ๆ

การแก้อยู่ที่บรรทัดล่างสุดเสมอ: zip ต้องเป็น number (parse มันหรือแก้ที่จุดที่ผลิต "10001" เป็น string ตั้งแต่แรก) การไล่แก้บรรทัดบนแทน — cast raw เป็น Person หรือทำให้ Person.address หลวมเป็น any — เพียงแค่ทำให้ message เงียบ โดยไม่ได้แตะ bug จริงเลย

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)"]
Error chain ที่ซ้อนกัน: อ่านจากล่างขึ้นบน
ตัวเลือกข้อดีต้นทุน
interface สำหรับ public shapeextend ได้, 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 เขียนแล้วเจอ 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 squiggle
console.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 เป็นจริง ๆ

type system ของ TypeScript ทำงานตอนไหน?
mental model ที่มีประโยชน์ที่สุดสำหรับ type ของ TypeScript คืออะไร?
สองไอเดียใดที่อธิบายพฤติกรรมของ TypeScript ได้เกือบทั้งหมด?
interface Config { name: string } ประกาศซ้ำที่อื่นด้วย interface Config { version: number } ได้ไหม?
Error ที่ได้เรียกชื่อ property ที่ขาดหายว่าอะไร และทำไม?
interface Base { id: string }
interface IUser extends Base { name: string }

type TBase = { id: string };
type TUser = TBase & { name: string };

function greetT(u: TUser) {}
greetT({ id: "1" });
บรรทัดไหนมีข้อเท็จจริงที่แก้ได้ และควรอ่านทิศทางไหน?
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'.
diff หนึ่ง "แก้" type error โดยเปลี่ยน const p: Person = raw; เป็น const p: Person = raw as unknown as Person; การแก้นี้แก้ปัญหาจริงที่ error เดิมรายงานไหม?