Skip to content

บทที่ 00 — TypeScript คืออะไร

สารบัญ | บทที่ 01: Setup →

⏱ ใช้เวลาอ่าน 45–60 นาที — บทนี้ยังไม่ต้องเขียนโค้ดตาม แค่ "ทำความเข้าใจภาพรวม" ให้แน่นก่อน

⚠️ TypeScript สร้างบน JavaScript — ถ้าคุณยังไม่รู้จัก JavaScript เลย แนะนำให้อ่าน JavaScript เล่ม ให้จบบทที่ 10 (Modules) ก่อน แล้วค่อยกลับมาที่บทนี้


บทนี้จะพาคุณไปเจออะไรบ้าง

ก่อนจะลงมือเขียน TypeScript เราต้องเข้าใจก่อนว่า มันเกิดมาเพื่อแก้ปัญหาอะไร ถ้าข้ามขั้นนี้ไป คุณจะเขียน TypeScript ได้แบบ "ทำตามสูตร" แต่ไม่เข้าใจว่าทำไมต้องทำแบบนั้น พอเจอ error แปลก ๆ ก็จะแก้ไม่เป็น

บทนี้จะตอบคำถามเหล่านี้ทีละข้อ:

  • "Type" คืออะไร ทำไมต้องสนใจ
  • JavaScript มีปัญหาอะไร TypeScript ถึงต้องเกิดมา
  • TypeScript ทำงานยังไง — มันรันได้ไหม
  • "Compile-time" กับ "Runtime" ต่างกันยังไง (อันนี้สำคัญมาก)
  • ทำไม type ของ TypeScript "หายไป" ตอนโปรแกรมรันจริง
  • เมื่อไหร่ควรใช้ TypeScript เมื่อไหร่ไม่ต้อง

อ่านจบบทนี้ คุณจะ "เก็ตภาพใหญ่" และพร้อมลงมือในบทถัดไป


Part 1: เริ่มจากปัญหาจริง

ลองนึกภาพว่าคุณเขียนเว็บไซต์ขายของ มีฟังก์ชันคำนวณยอดรวมในตะกร้าแบบนี้ (เขียนด้วย JavaScript ธรรมดา):

javascript
function calculateTotal(item, quantity) {
    return item.price * quantity;
}

ดูเผิน ๆ ก็ปกติดี ใช้งานแบบนี้:

javascript
calculateTotal({ price: 100 }, 3);   // ได้ 300 — ถูกต้อง

แต่ปัญหาคือ JavaScript ไม่เคยถามเลย ว่าค่าที่คุณส่งเข้าไปเป็นชนิดอะไร ลองดูกรณีนี้ — สมมุติว่าข้อมูล item มาจากฟอร์มหรือ API ที่ส่ง price มาเป็นข้อความ (string) แทนที่จะเป็นตัวเลข (number):

javascript
calculateTotal({ price: "100฿" }, 3);   // ได้ NaN 😱 (Not a Number — ไม่ใช่ตัวเลข)

เกิดอะไรขึ้น? เพราะ item.price เป็น ข้อความ "100" แทนที่จะเป็น ตัวเลข 100 พอเอามาคูณกับ quantity (3) ปกติ JavaScript จะพยายามแปลง "100" เป็นตัวเลขให้ — แต่ในตัวอย่างจริงบางสถานการณ์ (เช่น item.price เป็น "100฿" หรือ undefined เพราะ field ผิดชื่อ) ผลลัพธ์จะกลายเป็น NaN ทันที

🤔 เกร็ดเล็ก: "100" * 3 ใน JS จริง ๆ ได้ 300 (JS แปลง string เป็น number ให้) — แต่ "100฿" * 3 ได้ NaN, undefined * 3 ก็ NaN, และ "100" + 3 (ใช้ + แทน *) ได้ "1003" (ต่อ string) นี่คือพฤติกรรม "แปลกชนิด" ที่ TypeScript จะกัน

และที่แย่ที่สุดคือ — โปรแกรมไม่ฟ้อง error เลย มันรันผ่าน เอา NaN ไปบันทึกในระบบ ส่งราคาผิดให้ลูกค้า แล้วคุณจะรู้ตัวก็ต่อเมื่อลูกค้าโทรมาด่า หรือเห็นยอดเงินผิดในระบบบัญชี

นี่คือ ปัญหาหลัก ของ JavaScript: มันเป็นภาษาที่ "ใจดีเกินไป" — มันจะพยายามทำงานต่อให้ได้เสมอ แม้สิ่งที่คุณส่งเข้าไปจะผิดประเภทก็ตาม ผลคือความผิดพลาดถูก "ซ่อน" ไว้เงียบ ๆ จนกว่าจะไปโผล่ตอนใช้งานจริง

💡 ในวงการเรียกบั๊กแบบนี้ว่า "บั๊กเงียบ" (silent bug — สำนวนอังกฤษ) — มันไม่ส่งเสียงเตือน แต่สร้างความเสียหายอยู่เบื้องหลัง


Part 2: "Type" คืออะไร (ปูพื้นสำหรับคนที่ยังไม่รู้จัก)

ก่อนจะไปต่อ เราต้องเข้าใจคำว่า "type" (ชนิดข้อมูล) ให้ชัดก่อน

ในการเขียนโปรแกรม ข้อมูลทุกชิ้นมี "ชนิด" ของมัน เหมือนของในชีวิตจริงที่มีหมวดหมู่:

ข้อมูลชนิด (type)เปรียบเทียบ
42number (ตัวเลข)ของที่เอามาบวกลบคูณหารได้
"สวัสดี"string (ข้อความ)ของที่เอามาต่อกันเป็นประโยคได้
true / falseboolean (ค่าจริง/เท็จ)สวิตช์เปิด-ปิด
[1, 2, 3]array (อาเรย์)กล่องที่ใส่ของหลายชิ้นเรียงกัน
{ name: "A" }object (ออบเจกต์)กล่องที่ของแต่ละชิ้นมีป้ายชื่อ

ทำไม type ถึงสำคัญ? เพราะ แต่ละ type ทำอะไรได้ไม่เหมือนกัน:

  • ตัวเลข 5 เอามาคูณได้ → 5 * 2 = 10
  • ข้อความ "hello" เอามาคูณ → ไม่มีความหมาย ❌
  • ข้อความมี .length (ความยาว) ได้ → "hello".length = 5
  • ตัวเลขไม่มี .length

ถ้าเราเอา type มาใช้ผิดที่ ผลลัพธ์จะเพี้ยน — เหมือนเอากรรไกรไปตอกตะปู มันทำได้ แต่ผลลัพธ์ไม่ใช่ที่คุณต้องการ

JavaScript รู้จัก type (ทุกค่ามี type ของมันตอนรัน) แต่ปัญหาคือ JavaScript จะ ไม่บอกคุณล่วงหน้า ว่าคุณกำลังใช้ type ผิด มันรอให้โปรแกรมรันไปเจอจุดนั้นก่อนถึงจะมีปัญหา


Part 3: TypeScript เข้ามาช่วยยังไง

TypeScript คือ JavaScript ที่เพิ่ม "ระบบตรวจสอบ type" เข้ามา

แนวคิดง่ายมาก: แทนที่จะปล่อยให้รู้ตัวว่า type ผิดตอนโปรแกรมรัน เราจะให้ "ผู้ช่วย" คอยตรวจสอบโค้ดของเรา ตั้งแต่ตอนที่ยังเขียนอยู่ — ก่อนที่ลูกค้าจะได้เห็นด้วยซ้ำ

กลับมาที่ฟังก์ชันเดิม แต่คราวนี้เขียนด้วย TypeScript:

💡 interface คือวิธีหนึ่งที่ TypeScript ใช้บอกว่า object ต้องมี field อะไรบ้าง — รายละเอียดเต็มในบทที่ 04

typescript
interface Item {
    price: number;
}

function calculateTotal(item: Item, quantity: number): number {
    return item.price * quantity;
}

สังเกตส่วนที่เพิ่มเข้ามา:

  • item: Item — บอกว่า "พารามิเตอร์ตัวนี้ต้องเป็น object ที่มี price เป็นตัวเลข"
  • quantity: number — บอกว่า "พารามิเตอร์ตัวนี้ต้องเป็นตัวเลข"
  • : number ท้ายวงเล็บ — บอกว่า "ฟังก์ชันนี้จะคืนค่าเป็นตัวเลข"

ส่วนที่ขึ้นต้นด้วยเครื่องหมาย : เหล่านี้เรียกว่า type annotation (การกำกับชนิด) — เป็นการ "เขียนป้ายบอกชนิด" ให้กับข้อมูล (ส่วน interface คือการประกาศ "หน้าตา" ของ object — บทที่ 04 จะลงลึก)

ทีนี้ลองส่งค่าผิดดู:

typescript
calculateTotal({ price: "100" }, 3);
//               ~~~~~
// ❌ Error: Type 'string' is not assignable to type 'number'.
//    (แปล: ค่าชนิด string ส่งให้ field ชนิด number ไม่ได้)

คราวนี้ TypeScript ฟ้องทันที — เส้นหยัก ๆ สีแดงจะขึ้นใต้ "100" ใน editor ของคุณ ก่อนที่คุณจะรันโปรแกรมด้วยซ้ำ คุณแก้ได้เดี๋ยวนั้นเลย ลูกค้าไม่มีทางเจอบั๊กนี้

🎯 สิ่งสำคัญที่สุดของ TypeScript คือการทำให้เจอบั๊กเร็วขึ้น — เลื่อนเวลาที่เราพบบั๊กจาก "ตอนลูกค้าใช้งาน" มาเป็น "ตอนที่เรากำลังพิมพ์โค้ดอยู่" ยิ่งพบบั๊กเร็วเท่าไหร่ ยิ่งแก้ถูกและง่ายเท่านั้น

3.1 TypeScript คือ "เซตที่ครอบคลุม" (superset) ของ JavaScript

คำว่า "เซตที่ใหญ่กว่า" (superset) แปลตรงตัวว่า "เซตที่ใหญ่กว่าและบรรจุอีกเซตหนึ่งไว้ครบทั้งหมด" — TypeScript มีทุกอย่างที่ JavaScript ทำได้ (เซตเล็ก) แล้วเพิ่มของใหม่เข้าไปอีก (กลายเป็นเซตที่ใหญ่กว่า)

พูดอีกแบบ: โค้ด JavaScript ที่ถูกต้อง = โค้ด TypeScript ที่ถูกต้อง (เกือบทั้งหมด)

typescript
// นี่คือ JavaScript ธรรมดา — แต่ก็เป็น TypeScript ที่ใช้ได้เลย
const message = "Hello";
console.log(message.toUpperCase());

ข้อดีของเรื่องนี้คือ ถ้าคุณรู้ JavaScript อยู่แล้ว คุณ ไม่ต้องเรียนภาษาใหม่ทั้งหมด — คุณแค่เรียน "ส่วนที่เพิ่มเข้ามา" ซึ่งก็คือระบบ type นั่นเอง และนั่นคือสิ่งที่หนังสือเล่มนี้จะสอน

⚠️ เพราะเหตุนี้ คุณต้องรู้ JavaScript มาก่อน ถ้ายังไม่แม่น JavaScript แนะนำให้กลับไปอ่าน Javascript ก่อน แล้วค่อยกลับมา ไม่งั้นคุณจะแยกไม่ออกว่าอันไหนคือ JavaScript อันไหนคือ TypeScript


Part 4: ปัญหา 4 ข้อที่ TypeScript ช่วยแก้ได้จริง

หลายคนคิดว่า TypeScript มีไว้แค่ "กันพิมพ์ผิด" — จริง ๆ มันให้มากกว่านั้นเยอะ มาดูทีละข้อ

4.1 จับบั๊กได้ตั้งแต่เนิ่น ๆ

ในโปรเจกต์เล็ก ๆ บั๊กแบบ type ผิดอาจหาเจอง่าย แต่พอโปรเจกต์ใหญ่ขึ้น (เป็นหมื่นเป็นแสนบรรทัด) บั๊กพวกนี้จะหายากมาก ตัวอย่างบั๊กที่ TypeScript จับได้แต่ JavaScript ปล่อยผ่าน:

typescript
const user = { name: "Alice", age: 25 };

console.log(user.namw);   // ❌ TS Error: Property 'namw' does not exist
//                ~~~~
// พิมพ์ผิด! ตั้งใจจะพิมพ์ 'name' แต่พิมพ์ 'namw'

ใน JavaScript บรรทัดนี้จะไม่ error — มันจะแสดงผลเป็น undefined เงียบ ๆ แล้วคุณจะงงว่าทำไมชื่อไม่ขึ้น

อีกตัวอย่าง — ส่งพารามิเตอร์สลับลำดับ:

typescript
function divide(numerator: number, denominator: number) {
    return numerator / denominator;
}
// ถ้าวันหนึ่งมีคนเปลี่ยน divide ให้รับ 3 พารามิเตอร์
// โค้ดเก่าทุกที่ที่เรียก divide() ด้วย 2 พารามิเตอร์ จะถูก TS ฟ้องทันที

4.2 IDE ฉลาดขึ้นมหาศาล (ข้อนี้คนมักมองข้าม)

IDE คือโปรแกรมที่เราใช้เขียนโค้ด (เช่น VS Code) เมื่อใช้ TypeScript IDE จะ "รู้จัก" โค้ดของคุณลึกขึ้นมาก

ตัวอย่างที่ 1 — Autocomplete (เติมคำอัตโนมัติ):

typescript
const user = { name: "Alice", age: 25, email: "a@x.com" };

user.
//   ↑ พอพิมพ์จุด IDE จะเด้งรายการ "name", "age", "email" ขึ้นมาให้เลือก
//     คุณไม่ต้องจำเอง ไม่ต้องเปิดไปดูว่า object นี้มี field อะไรบ้าง

ตัวอย่างที่ 2 — Rename ทั้งโปรเจกต์ในคลิกเดียว:

ถ้าคุณอยากเปลี่ยนชื่อ field จาก name เป็น fullName ทั้งโปรเจกต์ TypeScript รู้แน่ชัดว่า name ตัวไหนหมายถึง field นี้ (ไม่ใช่ name ที่บังเอิญชื่อซ้ำ) IDE จึงแก้ให้ถูกทุกที่ได้อย่างมั่นใจ

ใน JavaScript การ rename แบบนี้คือการ "ค้นหา-แทนที่ข้อความ" ซึ่งเสี่ยงแก้โดนตัวที่ชื่อซ้ำกันโดยไม่ตั้งใจ

4.3 โค้ดอธิบายตัวเองได้ (เป็นเอกสารในตัว)

ลองดูฟังก์ชัน 2 แบบนี้ แล้วถามตัวเองว่าอันไหนเข้าใจง่ายกว่า:

javascript
// JavaScript — ต้องเดาเอาว่า id เป็นอะไร คืนค่าอะไร
function fetchUser(id) { ... }
typescript
// TypeScript — อ่านแล้วรู้เลย
function fetchUser(id: number): Promise<User> { ... }
//                  ↑รับเลข     ↑คืน Promise ที่ห่อ User ไว้

💡 async / Promise คืออะไร (แบบย่อ): ฟังก์ชันบางอย่างต้อง "รอ" (เช่น รอข้อมูลจากเซิร์ฟเวอร์) แทนที่จะได้คำตอบทันที เราเรียกฟังก์ชันแบบนี้ว่า async (ทำงานแบบ asynchronous — ไม่รอสิ้นสุดทันทีในบรรทัดเดียว) และค่าที่มันคืนกลับมาจะห่อไว้ใน Promise (สัญญาว่าจะได้ค่าจริงในอนาคต) ดังนั้น Promise<User> แปลว่า "สัญญาว่าจะได้ User กลับมา (ไม่ใช่ตอนนี้ทันที)" — ถ้ายังไม่คุ้นเรื่องนี้ อ่านเพิ่มได้ในบทที่ 03 และ JavaScript บทที่ 09-10 ตอนนี้จำแค่ว่า TypeScript ระบุ return type ให้ฟังก์ชัน async ได้เหมือนฟังก์ชันปกติ

แค่อ่านบรรทัดแรก คุณก็รู้แล้วว่า "ฟังก์ชันนี้รับเลข id แล้วคืนข้อมูล User แบบ asynchronous" โดยไม่ต้องเปิดเข้าไปอ่านโค้ดข้างใน type จึงทำหน้าที่เป็น เอกสารที่ไม่มีวันล้าสมัย (เพราะถ้า type ไม่ตรงกับโค้ดจริง TypeScript จะฟ้อง — ต่างจาก comment ที่อาจลืมอัปเดต)

4.4 กล้า refactor (ปรับโครงสร้างโค้ด)

Refactor = การปรับปรุงโครงสร้างโค้ดให้ดีขึ้นโดยไม่เปลี่ยนพฤติกรรม

ในโปรเจกต์ JavaScript ขนาดใหญ่ การ refactor น่ากลัวมาก เพราะคุณไม่มีทางรู้ว่าการแก้จุดหนึ่งจะไปทำให้อีก 50 จุดพังหรือเปล่า หลายทีมจึง "ไม่กล้าแตะโค้ดเก่า" จนโค้ดเน่าไปเรื่อย ๆ

ใน TypeScript เมื่อคุณแก้ type ของอะไรสักอย่าง คอมไพเลอร์จะ ชี้ให้เห็นทุกจุดที่พัง ทันที คุณแค่ไล่แก้ตามจุดสีแดงจนหมด ก็มั่นใจได้ว่า refactor สำเร็จ ความมั่นใจนี้คือสิ่งที่ทำให้ทีมกล้าพัฒนาโค้ดต่อในระยะยาว


Part 5: TypeScript ทำงานยังไง — เรื่อง Compile-time vs Runtime

หัวข้อนี้สำคัญที่สุดในบท ถ้าเข้าใจตรงนี้ คุณจะเข้าใจ "ขอบเขต" ของ TypeScript ว่ามันช่วยอะไรได้ และอะไรที่มันช่วยไม่ได้

5.1 สองช่วงเวลาในชีวิตของโปรแกรม

โปรแกรมหนึ่งตัวมี "ช่วงเวลา" สำคัญ 2 ช่วง:

1. Compile-time (ช่วงคอมไพล์) — ช่วงที่โค้ดของคุณ "ถูกแปลง/ตรวจสอบ" ก่อน นำไปรัน เปรียบเหมือนช่วงที่บรรณาธิการตรวจต้นฉบับหนังสือก่อนส่งโรงพิมพ์

2. Runtime (ช่วงรัน) — ช่วงที่โปรแกรม "ทำงานจริง" บนเครื่องผู้ใช้ เปรียบเหมือนช่วงที่ผู้อ่านกำลังอ่านหนังสือที่พิมพ์เสร็จแล้ว

TypeScript ทำงานเฉพาะใน "compile-time" เท่านั้น — มันคือบรรณาธิการที่ตรวจต้นฉบับ พอหนังสือพิมพ์เสร็จแล้ว บรรณาธิการก็ไม่ได้ตามไปนั่งข้างผู้อ่าน

5.2 เบราว์เซอร์รัน TypeScript ไม่ได้

ความจริงที่หลายคนตกใจ: เบราว์เซอร์และ Node.js รัน TypeScript ไม่ได้โดยตรง มันรู้จักแต่ JavaScript เท่านั้น

ดังนั้นก่อนนำโค้ดไปใช้จริง ต้องผ่านขั้นตอน compile (บางทีเรียก transpile) เพื่อแปลง .ts ให้กลายเป็น .js ก่อน:

ไฟล์ที่คุณเขียน → ตัวแปลง → ไฟล์ที่เครื่องรันจริง:

tsc (TypeScript Compiler) คือเครื่องมือที่ทำหน้าที่นี้ — มันทำ 2 อย่างพร้อมกัน:

  1. ตรวจ type — มีอะไรผิดไหม ถ้าผิดก็ฟ้อง
  2. ลบ type ออก แล้วเซฟเป็นไฟล์ .js

5.3 ตอน compile เกิดอะไรขึ้น — "Type Erasure"

มาดูกันชัด ๆ ว่าโค้ด TypeScript กลายเป็น JavaScript หน้าตายังไง

โค้ดที่คุณเขียน (foo.ts):

typescript
function add(a: number, b: number): number {
    return a + b;
}
const result: number = add(1, 2);

หลังผ่าน tsc แล้ว (foo.js):

javascript
function add(a, b) {
    return a + b;
}
const result = add(1, 2);

สังเกตให้ดี — type annotation ทั้งหมดหายไปหมด! : number ทุกตัวถูกลบทิ้ง เหลือแต่ JavaScript ล้วน ๆ

ปรากฏการณ์นี้เรียกว่า "การลบชนิดทิ้ง" (Type Erasure) และมันคือกุญแจสำคัญที่ต้องจำ:

🔑 ตอนโปรแกรมรันจริง (runtime) ไม่มี type ของ TypeScript เหลืออยู่เลย — type มีชีวิตอยู่แค่ตอนเขียนโค้ดและตอน compile เท่านั้น

5.4 ทำไมเรื่องนี้ถึงสำคัญมาก

เพราะมันแปลว่า TypeScript มีขอบเขตการปกป้องที่ชัดเจน:

  • ปกป้องได้: ข้อมูลที่เกิดขึ้นภายในโค้ดของคุณเอง
  • ปกป้องไม่ได้: ข้อมูลที่มาจากโลกภายนอก

ลองคิดดู — ข้อมูลที่มาจากภายนอกโปรแกรม เช่น:

  • คำตอบจาก API (เซิร์ฟเวอร์)
  • ข้อมูลที่ผู้ใช้กรอกในฟอร์ม
  • ไฟล์ที่อ่านจากดิสก์
  • ข้อมูลจากฐานข้อมูล

ข้อมูลพวกนี้เข้ามาตอน runtime ซึ่งเป็นช่วงที่ TypeScript ไม่อยู่แล้ว ตัวอย่าง:

typescript
interface User {
    id: number;
    name: string;
}

// ⚠️ นี่คือโค้ดตัวอย่างเพื่ออธิบายแนวคิด ไม่ได้ให้ copy ไปรันตรง ๆ
// (ต้องอยู่ใน async function จริง ๆ ถึงจะรันได้ — รายละเอียดบทที่ 01/11)
// เราบอก TypeScript ว่า "เชื่อเถอะ ข้อมูลที่ได้จาก API เป็น User"
// (ตัวอย่างนี้ใช้ interface, await, Promise — ถ้ายังไม่แม่น ดูใน Javascript book บทที่ 9-10 ก่อน)
const user: User = await fetch("/api/user").then(r => r.json());
// หมายเหตุ: top-level await ต้องการ "type": "module" ใน package.json — รายละเอียดในบทที่ 01 และ 11

console.log(user.name.toUpperCase());

⚠️ จุดอันตราย: ข้อมูลจาก API มาจากภายนอก TypeScript ตรวจสอบให้ไม่ได้ตอน runtime — เรื่องนี้อธิบายเต็มในบทที่ 11

โค้ดนี้ TypeScript ไม่ฟ้องอะไรเลย เพราะเราบอกมันว่า user เป็น User แต่ความจริงคือ — TypeScript ไม่มีทางรู้ ว่าเซิร์ฟเวอร์ส่งอะไรกลับมาจริง ๆ ถ้าเซิร์ฟเวอร์ส่ง { id: 1 } มา (ไม่มี name) บรรทัด user.name.toUpperCase() จะพังตอน runtime ทันที

📌 บทเรียนสำคัญ: type ของ TypeScript เป็นเหมือน "สัญญา" ที่เราเขียนกำกับไว้ มันไม่ใช่ "เกราะ" ที่บังคับใช้ตอน runtime ดังนั้นข้อมูลที่มาจากภายนอก ต้องตรวจสอบด้วยโค้ดจริง ๆ เสมอ (เดี๋ยวจะพูดถึงเครื่องมือที่ช่วยตรงนี้ใน Part 9)

5.5 ยุคใหม่ — bundler ลบ type ให้เลย

💡 bundler (บันเดิลเลอร์) = เครื่องมือรวมไฟล์โค้ดหลายไฟล์ให้กลายเป็นชุดเดียวที่พร้อมส่งให้เบราว์เซอร์

ในปี 2026 bundler สมัยใหม่อย่าง Vite, esbuild, Bun ฉลาดพอที่จะ "ลบ type ทิ้ง" ได้เองเร็วมาก โดยไม่ต้องเรียก tsc ให้ compile

ดังนั้น workflow ที่นิยมในปัจจุบันคือ แยกหน้าที่กัน:

bash
tsc --noEmit      # ใช้ tsc แค่ "ตรวจ type" อย่างเดียว (--noEmit = อย่าสร้างไฟล์ออกมา)
vite build        # ใช้ Vite สร้างไฟล์จริง (ลบ type ทิ้งด้วย esbuild ในตัว)

ไม่ต้องเข้าใจรายละเอียดตอนนี้ก็ได้ แค่จำหลักการว่า "ตรวจ type" กับ "สร้างไฟล์" เป็นคนละงานกัน และมักทำด้วยเครื่องมือคนละตัว


Part 6: "เทียบที่โครงสร้าง" (Structural Typing) — TypeScript ดู "หน้าตา" ไม่ดู "ชื่อ"

นี่คือแนวคิดเฉพาะตัวของ TypeScript ที่ต่างจากภาษาอย่าง Java หรือ C# มาก ถ้าคุณเคยเรียนภาษาพวกนั้นมา อาจต้องปรับความคิดนิดหน่อย

6.1 ปัญหา: type ตรงกันเมื่อไหร่?

คำถามคือ — เวลา TypeScript ตัดสินว่า "ค่านี้เป็น type นี้ได้ไหม" มันใช้เกณฑ์อะไร?

มี 2 แนวคิดในโลกของภาษาโปรแกรม:

  • Nominal typing (ดูที่ "ชื่อ") — สองอย่างจะ type เดียวกันได้ ก็ต่อเมื่อมัน "ประกาศชื่อ type เดียวกัน" — แบบ Java, C#
  • Structural typing (ดูที่ "โครงสร้าง/หน้าตา") — สองอย่างจะ type เดียวกันได้ ถ้ามัน "มีหน้าตาเหมือนกัน" ไม่สนว่าชื่อ type อะไร — แบบ TypeScript

6.2 ตัวอย่าง

typescript
interface Point {
    x: number;
    y: number;
}

function distance(p: Point): number {
    return Math.sqrt(p.x ** 2 + p.y ** 2);
}

ฟังก์ชัน distance ขอรับค่า type Point ลองดูว่าอะไรส่งเข้าไปได้บ้าง:

typescript
// 1. object ธรรมดาที่มี x, y — ✅ ผ่าน แม้ไม่เคยบอกว่ามันเป็น Point
distance({ x: 3, y: 4 });

// 2. object ที่มี x, y ครบ แต่มี z เกินมา — ✅ ก็ยังผ่าน!
const point3D = { x: 3, y: 4, z: 5 };
distance(point3D);

// 3. instance ของ class ที่บังเอิญมี x, y — ✅ ผ่าน
class Vector {
    constructor(public x: number, public y: number) {}
}
distance(new Vector(3, 4));

ทั้ง 3 กรณีผ่านหมด เพราะ TypeScript ดูแค่ว่า "ค่านี้มี x: number และ y: number ครบไหม" ถ้าครบก็ถือว่าใช้แทน Point ได้ ไม่สนว่าจะชื่ออะไร มาจาก class ไหน หรือมี field อื่นเกินมา

แนวคิดนี้บางทีเรียกว่า "duck typing" (จัดประเภทแบบเป็ด) — เป็นสำนวนฝรั่งคลาสสิกในวงการโปรแกรม:

"ถ้ามันเดินเหมือนเป็ด และร้องเหมือนเป็ด เราก็เรียกมันว่าเป็ด" — ไม่ต้องสนว่ามันถูกประกาศชื่อว่า "เป็ด" หรือเปล่า

พูดง่าย ๆ คือ TypeScript ตัดสินจากสิ่งที่ object ทำได้ (มี field อะไรบ้าง) ไม่ใช่จากชื่อ type ที่ประกาศไว้

6.3 ทำไมต้องรู้เรื่องนี้

เพราะมันส่งผลต่อวิธีคิดของคุณ ใน TypeScript คุณ ไม่จำเป็นต้องประกาศว่า "object ของฉันเป็น type X" อย่างเป็นทางการ ขอแค่หน้าตาตรงก็พอ ทำให้ TypeScript ยืดหยุ่นมาก เขียนสั้น แต่ก็ต้องระวังว่าบางทีของที่ "หน้าตาเหมือนกันโดยบังเอิญ" อาจถูกมองว่าเป็น type เดียวกันทั้งที่ความหมายต่างกัน (เช่น UserId กับ ProductId ที่ต่างก็เป็นแค่ number — TypeScript จะมองว่าสลับกันใช้ได้ทั้งที่ไม่ควร) เราจะมีเทคนิคแก้เรื่องนี้ในบทที่ 10 เรียกว่า "branded type" (การ "ตีตรา" type ให้ต่างกันแม้หน้าตาเหมือนกัน เพื่อกันสลับผิด)


Part 7: โลกของ Type กับโลกของ Value

อีกแนวคิดที่ต้องเข้าใจ — ในไฟล์ TypeScript หนึ่งไฟล์ จริง ๆ แล้วมี "สองโลก" ซ้อนกันอยู่

7.1 สองโลก

โลกของ Value (ค่า) — สิ่งที่ "มีอยู่จริง" ตอนโปรแกรมรัน เอามาใช้งานได้ เช่น ตัวแปร, ฟังก์ชัน, ค่าตัวเลข

โลกของ Type (ชนิด) — สิ่งที่อธิบาย "รูปร่าง" ของค่า มีอยู่แค่ตอน compile แล้วถูกลบทิ้ง (จำ Type Erasure ได้ไหม)

typescript
// === โลกของ Type === (จะหายไปตอน runtime)
type UserId = number;
interface Product {
    name: string;
    price: number;
}

// === โลกของ Value === (มีอยู่จริงตอน runtime)
const myId = 42;
function greet() { return "hi"; }

type และ interface อยู่ในโลกของ type — เขียนไว้ช่วยตรวจสอบ แต่พอ compile เสร็จจะหายไปหมด ส่วน const และ function อยู่ในโลกของ value — มันกลายเป็น JavaScript จริง ๆ

7.2 ข้อยกเว้น: class อยู่ทั้งสองโลก

มีของอยู่อย่างหนึ่งที่พิเศษ — class มันมีตัวตนอยู่ ทั้งสองโลกพร้อมกัน:

typescript
class User {
    constructor(public name: string) {}
}

// ใช้ User ในฐานะ "type" (โลกของ type)
const u: User = new User("Alice");
//        ↑ตรงนี้ User คือ type

// ใช้ User ในฐานะ "value" (โลกของ value)
const u2 = new User("Bob");
//             ↑ตรงนี้ User คือ value (เอามา new ได้)

เพราะ class ตอน compile แล้วยังเหลือเป็นโค้ด JavaScript จริง (ต่างจาก interface ที่หายไป) แต่ในขณะเดียวกันมันก็อธิบายรูปร่างของ object ได้ด้วย จึงเป็น type ได้ด้วย

ตอนนี้ยังไม่ต้องจำลึก แค่รู้ว่า "ของบางอย่างเป็น type, บางอย่างเป็น value, และ class เป็นทั้งคู่" — เดี๋ยวเราจะเจอเรื่องนี้อีกในบทหลัง ๆ


Part 8: ข้อดี-ข้อเสียของ TypeScript (แบบมีเหตุผล)

ไม่มีเครื่องมือไหนสมบูรณ์แบบ มาดูกันตรง ๆ ว่า TypeScript แลกอะไรกับอะไร

8.1 ข้อดี

ข้อดีอธิบาย
✅ จับบั๊กตั้งแต่ compile-timeบั๊กเรื่อง type ถูกจับก่อนถึงมือผู้ใช้
IDE ช่วยเหลือเต็มที่autocomplete, rename, jump-to-definition แม่นยำ
✅ โค้ดอธิบายตัวเองtype คือเอกสารที่อัปเดตเสมอ
✅ refactor ได้อย่างมั่นใจคอมไพเลอร์ชี้ทุกจุดที่พัง
✅ ทำงานเป็นทีมง่ายขึ้นtype คือ "สัญญา" ที่ชัดเจนระหว่างคนเขียนโค้ดคนละส่วน

8.2 ข้อเสีย (และมุมมองที่ถูกต้อง)

ข้อเสียอธิบาย / มุมมอง
❌ ต้องเรียนรู้เพิ่มมี learning curve — แต่นั่นคือเหตุผลที่คุณกำลังอ่านหนังสือเล่มนี้
❌ มีขั้นตอน buildต้อง compile ก่อนรัน — แต่เครื่องมือสมัยใหม่เร็วมากจนแทบไม่รู้สึก
❌ ต้องหา type ให้ librarylibrary เก่าบางตัวต้องลง @types/* เพิ่ม
❌ บางครั้ง "สู้กับ type system"บางสถานการณ์ที่ type ซับซ้อนจน TS งง — แก้ได้ด้วยประสบการณ์
❌ ไม่ปกป้อง runtimetype หายตอนรัน — ต้อง validate ข้อมูลภายนอกเอง (Part 5.4)

💬 มุมมองที่อยากให้คิด: ข้อเสียส่วนใหญ่เป็นการ "ลงทุนล่วงหน้า" — คุณจ่ายความพยายามตอนเขียน เพื่อแลกกับความสบายตอนดูแลรักษาในระยะยาว สำหรับโปรเจกต์ที่อยู่ไปนาน ๆ การลงทุนนี้คุ้มเสมอ


Part 9: เมื่อไหร่ควรใช้ / ไม่ควรใช้ TypeScript

9.1 ควรใช้ TypeScript เมื่อ

  • ✅ ทำงานเป็นทีม — type ช่วยให้ทุกคนเข้าใจตรงกัน
  • ✅ โปรเจกต์มีโค้ดเยอะ (เกิน ~1,000 บรรทัด)
  • ✅ เขียน library ให้คนอื่นใช้ — type คือเอกสารการใช้งาน
  • ✅ โปรเจกต์ที่ต้องดูแลระยะยาว
  • ✅ อยากได้ IDE ที่ฉลาด

9.2 อาจไม่จำเป็นต้องใช้ เมื่อ

  • ❌ สคริปต์เล็ก ๆ ใช้ครั้งเดียวทิ้ง
  • ❌ ต้นแบบ (prototype) ที่ทำเร็ว ๆ เพื่อทดลองไอเดีย
  • ❌ คุณกำลังเรียน JavaScript พื้นฐานอยู่ — เรียน JS ให้แม่นก่อน

9.3 ความจริงของปี 2026

ในวงการพัฒนาซอฟต์แวร์ปัจจุบัน:

  • เว็บโปรเจกต์ใหม่ส่วนใหญ่เลือกใช้ TypeScript เป็นค่าเริ่มต้น ตามผลสำรวจ State of JS (ผลสำรวจนักพัฒนา JS รายปี) และ npm trends (กราฟความนิยมของแพ็กเกจ)

  • เฟรมเวิร์กใหญ่ ๆ — React, Next.js, Angular, Vue — ออกแบบมาให้ใช้ TypeScript เป็นหลัก

  • Node.js รันไฟล์ .ts ได้แล้ว — Node 22 ต้องใช้ flag --experimental-strip-types ก่อน แล้วเปิดเป็นค่า default (ไม่ต้องใส่ flag) ตั้งแต่ Node 23.6 ขึ้นไป (รายละเอียดคำสั่งจริงดูในบทที่ 01) ส่วน Bun และ Deno รองรับ TypeScript เต็มตัวมาตั้งแต่ต้น

    💡 Node.js 22+ สามารถรันไฟล์ .ts ได้บางแบบ แต่มีข้อจำกัดบางอย่าง เช่น enum (ชนิดข้อมูลกลุ่มค่าคงที่) และ namespace (กรอบชื่อแบบเก่าของ TS) — รายละเอียดในบทที่ 01 และ 09 ⚠️ สำคัญ: การ "รัน .ts ได้" ไม่ได้แปลว่า type-check ให้ — strip-types แค่ลบ annotation ทิ้งแล้วรันเท่านั้น คุณยังต้องเรียก tsc --noEmit แยกเพื่อตรวจ type จริง ๆ ⚠️ ตรวจสอบเวอร์ชันล่าสุดก่อนใช้งานจริง — ฟีเจอร์นี้เปลี่ยนเร็ว ให้เช็ก Node.js release notes ทางการก่อนนำไปใช้ใน production

🌏 สรุปสั้น ๆ ของยุคนี้: "TypeScript คือ JavaScript เวอร์ชันมืออาชีพ" — ถ้าคุณตั้งใจทำงานสายนี้เป็นอาชีพ TypeScript ไม่ใช่ทางเลือก แต่เป็นทักษะพื้นฐานที่ต้องมี


Part 10: เครื่องมือรอบตัว TypeScript (Ecosystem)

ทำความรู้จักเครื่องมือที่จะเจอบ่อย ๆ ไว้คร่าว ๆ (เดี๋ยวบทที่ 01 จะลงมือติดตั้งจริง)

10.1 เครื่องมือหลัก

  • tsc — TypeScript Compiler ตัวจริง ใช้ตรวจ type และ/หรือ compile
  • tsx — เครื่องมือที่ให้เรา "รันไฟล์ .ts ได้เลย" ตอน develop (สะดวก ไม่ต้อง compile เอง)
  • VS Code — editor ที่รองรับ TypeScript ดีที่สุด (มี TypeScript ติดตั้งมาในตัว)

10.2 Type definitions และ @types/*

library เก่า ๆ ที่เขียนด้วย JavaScript ล้วน จะไม่มีข้อมูล type มาให้ ทำให้ TypeScript ไม่รู้จักหน้าตาของมัน วิธีแก้คือลง "ไฟล์ type แยก" ที่ชุมชนช่วยกันเขียนไว้:

bash
npm install lodash           # ตัว library
npm install -D @types/lodash # ไฟล์ type ของ lodash (-D = สำหรับ dev เท่านั้น)

ส่วน library สมัยใหม่ (เช่น React) มักจะมี type มาให้ในตัวแล้ว ไม่ต้องลง @types/* เพิ่ม

10.3 Library ตรวจสอบข้อมูลตอน runtime

จำ Part 5.4 ได้ไหม — type ของ TypeScript ไม่ปกป้องข้อมูลจากภายนอก เราจึงต้องมีเครื่องมือตรวจสอบข้อมูลตอน runtime จริง ๆ ตัวที่นิยมที่สุดคือ Zod:

📦 ก่อนใช้งาน Zod ต้องติดตั้งก่อน: npm install zod — ตัวอย่างด้านล่างเป็นภาพรวม เดี๋ยวบทที่ 11 จะลงมือใช้งานจริง

typescript
import { z } from "zod";

// 1. ประกาศ "schema" (สคีมา — กฎ/แม่แบบที่อธิบายว่าข้อมูลต้องหน้าตาแบบไหน)
const UserSchema = z.object({
    id: z.number(),
    email: z.string().email(),   // ต้องเป็น string และเป็นรูปแบบอีเมล
});

// 2. "infer" type ของ TypeScript ออกจาก schema ได้เลย (infer = อนุมาน/สรุปมาเอง — ไม่ต้องเขียน type ซ้ำ)
//    syntax `z.infer<typeof UserSchema>` ใช้ generic + typeof — ตอนนี้แค่รู้ว่า "ใช้ตามนี้" บทที่ 05 และ 11 จะลงลึก
type User = z.infer<typeof UserSchema>;

// 3. ตอน runtime — เอาข้อมูลจาก API มา "ตรวจจริง"
const data = await fetch("/api/user").then(r => r.json());
const user = UserSchema.parse(data);
// ถ้า data ไม่ตรง schema → โยน error ทันที (ไม่ปล่อยให้บั๊กลามไปไกล)
// ถ้าตรง → user จะมี type เป็น User ให้อัตโนมัติ

ความเจ๋งของ Zod คือมันทำให้ "การตรวจ runtime" กับ "type ของ TypeScript" เป็นเรื่องเดียวกัน — เขียนกฎที่เดียว ได้ทั้งการตรวจจริงและ type ตอน compile ในตอนนี้แค่รู้จักชื่อไว้ก่อน เดี๋ยวบทที่ 11 จะใช้งานจริง


Part 11: แผนการเดินทางของหนังสือเล่มนี้

หนังสือเล่มนี้ออกแบบให้อ่านเรียงตามลำดับ ค่อย ๆ ไต่จากพื้นฐานไปสู่ระดับสูง:

text
บท 0   Intro            ← คุณอยู่ตรงนี้ (เข้าใจภาพรวม)
บท 1   Setup            ติดตั้งเครื่องมือ, ทำความรู้จัก tsconfig.json
บท 2   Basic Types      type พื้นฐาน — number, string, array, tuple, union ฯลฯ
บท 3   Functions        การใส่ type ให้ฟังก์ชัน, overload
บท 4   Object & Interface  อธิบายรูปร่างของ object
บท 5   Generics         "type ที่ยืดหยุ่น" — หัวใจของ TS ขั้นกลาง
บท 6   Utility Types    เครื่องมือแปลง type สำเร็จรูป
บท 7   Narrowing        การ "ถาม type" ให้ TS รู้จริง ๆ
บท 8   Class            class กับ type system
บท 9   Module & .d.ts   การจัดระเบียบไฟล์, declaration file
บท 10  Advanced         type-level programming ของจริง
บท 11  In Practice      ใช้ TS กับงานจริง — React, Node, migration

แต่ละบทมี Checkpoint (คำถามทบทวน) และ Lab (โจทย์ให้ลงมือ) — อย่าข้าม เพราะ TypeScript เป็นทักษะที่ "เข้าใจจากการอ่าน" ไม่พอ ต้อง "ลงมือพิมพ์" ถึงจะซึม


Part 12: Checkpoint — ทบทวนความเข้าใจ

ลองตอบในใจก่อน แล้วค่อยกางเฉลยด้านล่างมาเทียบ ถ้าตอบไม่ได้เกินครึ่ง แนะนำให้อ่านบทนี้ซ้ำ

  1. TypeScript ต่างจาก JavaScript ตรงไหน?
  2. "Type erasure" คืออะไร และมันส่งผลอะไรกับเรา?
  3. "Compile-time" กับ "runtime" ต่างกันยังไง?
  4. ทำไมต้องใช้ Zod ทั้งที่มี TypeScript อยู่แล้ว?
  5. "Structural typing" คืออะไร ต่างจาก "nominal typing" ยังไง?
  6. ทำไม class ถึงเป็นทั้ง type และ value?
  7. ควรใช้ TypeScript ตอนไหน ไม่ควรใช้ตอนไหน?
  8. tsc --noEmit ทำอะไร?
  9. @types/* package มีไว้ทำไม?
👉 กดดูเฉลย
  1. TypeScript = JavaScript + ระบบ type ที่ตรวจสอบความถูกต้องของชนิดข้อมูลตอน compile-time ส่วนตัวภาษาและการทำงานตอนรันเหมือน JavaScript ทุกอย่าง
  2. Type erasure = ตอน compile type ทั้งหมดถูกลบทิ้ง โค้ดที่รันจริงคือ JavaScript ล้วน → ผลคือ type ของ TS ไม่มีอยู่ตอน runtime จึงปกป้องข้อมูลจากภายนอกไม่ได้ ต้อง validate เอง
  3. Compile-time = ช่วงตรวจ/แปลงโค้ดก่อนนำไปรัน (TypeScript ทำงานช่วงนี้) Runtime = ช่วงโปรแกรมทำงานจริงบนเครื่องผู้ใช้ (TypeScript ไม่อยู่แล้ว)
  4. เพราะ type ของ TS หายตอน runtime — ข้อมูลจาก API/ฟอร์ม/ไฟล์ TS เช็กให้ไม่ได้ Zod ตรวจสอบข้อมูลจริงตอน runtime และ infer type ให้ด้วย
  5. Structural typing = type ตรงกันถ้า "หน้าตา/โครงสร้างเหมือนกัน" (TS ใช้แบบนี้) Nominal typing = type ตรงกันก็ต่อเมื่อ "ประกาศชื่อ type เดียวกัน" (Java/C# ใช้แบบนี้)
  6. เพราะ class ตอน compile แล้ว ยังเหลือเป็นโค้ด JavaScript จริง (จึงเป็น value — เอาไป new ได้) และในขณะเดียวกันมันก็ อธิบายรูปร่าง object ได้ (จึงเป็น type)
  7. ควรใช้ เมื่อทำงานเป็นทีม / โปรเจกต์ใหญ่ / library / โปรเจกต์ระยะยาว ไม่จำเป็น เมื่อเป็นสคริปต์เล็ก ๆ ใช้แล้วทิ้ง หรือต้นแบบเร็ว ๆ
  8. tsc --noEmit สั่งให้ tsc ตรวจ type อย่างเดียว ไม่ต้องสร้างไฟล์ .js ออกมา — ใช้คู่กับ bundler ที่สร้างไฟล์ให้แทน
  9. @types/* คือ ไฟล์ type แยก สำหรับ library เก่าที่เขียนด้วย JS ล้วนและไม่มี type มาให้ — ทำให้ TypeScript รู้จักหน้าตาของ library นั้น

Part 13: สรุปบทนี้

สิ่งที่ต้องจำให้ขึ้นใจก่อนไปบทต่อไป:

  • TypeScript = JavaScript + ระบบ type ที่ทำงานตอน compile-time
  • หัวใจคือ "พบบั๊กเร็วขึ้น" — เลื่อนจากตอนผู้ใช้เจอ มาเป็นตอนกำลังพิมพ์โค้ด
  • Type Erasure — type หายไปหมดตอน runtime → ข้อมูลจากภายนอกต้อง validate เอง (เช่นใช้ Zod)
  • Structural typing — TypeScript ดู "หน้าตา" ของค่า ไม่ดูชื่อ type
  • มีสองโลก — โลกของ type (หายตอนรัน) กับโลกของ value (มีจริงตอนรัน) และ class อยู่ทั้งสองโลก
  • ปี 2026 — TypeScript คือมาตรฐานของงานมืออาชีพ

พร้อมแล้ว ไปติดตั้งเครื่องมือและเขียนโค้ดจริงกันในบทถัดไป


สารบัญ | ➡️ บทที่ 01: Setup →


🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-12