โหมดมืด
บทที่ 02 — Basic Types (ชนิดข้อมูลพื้นฐาน)
⏱ ใช้เวลา 2–3 ชั่วโมง — เปิด TypeScript Playground ไว้ พิมพ์โค้ดตามทุกตัวอย่าง
📌 เวอร์ชันอ้างอิง: ตัวอย่างในบทนี้ทดสอบบน TypeScript 5.7 (ตามที่ตั้งใน บทที่ 01) ฟีเจอร์บางตัวที่บทอื่นใช้ต้องการเวอร์ชันสูงกว่านี้ — จะระบุไว้กำกับเมื่อใช้
บทนี้สำคัญแค่ไหน
นี่คือบท "ตัวอักษร ก-ฮ" ของ TypeScript — เป็นพื้นฐานที่ทุกบทหลังจากนี้จะอ้างถึงตลอด ถ้าบทนี้ไม่แน่น บทต่อ ๆ ไปจะลำบาก ดังนั้นใจเย็น ๆ อ่านช้า ๆ และ พิมพ์โค้ดตามจริง อย่าแค่อ่านผ่าน
เราจะเรียน "อิฐก้อนแรก ๆ" ที่ใช้สร้างทุกอย่างใน TypeScript:
- ชนิดข้อมูลพื้นฐาน (number, string, boolean ฯลฯ)
- การประกาศ type เอง vs ปล่อยให้ TypeScript เดา
- array, tuple, literal type
any,unknown,never— สามตัวพิเศษที่ต้องเข้าใจให้ลึก- การประกอบ type เข้าด้วยกัน — union กับ intersection
Part 1: ชนิดข้อมูลพื้นฐาน (Primitive Types)
"Primitive" แปลว่า "พื้นฐานที่สุด แบ่งย่อยไม่ได้แล้ว" — เป็นค่าเดี่ยว ๆ ไม่ใช่กลุ่มของหลายค่า JavaScript มี primitive อยู่ 7 ชนิด และ TypeScript มีชื่อ type ให้แต่ละชนิด
typescript
let age: number = 42; // (1) number — ตัวเลข (ทั้งจำนวนเต็มและทศนิยม)
let name: string = "สมชาย"; // (2) string — ข้อความ
let isActive: boolean = true; // (3) boolean — ค่าจริง/เท็จ มีแค่ true กับ false
let big: bigint = 123n; // (4) bigint — ตัวเลขจำนวนเต็มขนาดใหญ่มาก (สังเกต n ต่อท้าย)
let id: symbol = Symbol("id"); // (5) symbol — ค่าที่ "ไม่ซ้ำใคร" เสมอ (ใช้น้อย)
let nothing: undefined = undefined; // (6) undefined — "ยังไม่มีค่า"
let empty: null = null; // (7) null — "จงใจให้ว่าง"3 ตัวบนสุด (number, string, boolean) คือสิ่งที่คุณจะใช้ 95% ของเวลา ส่วน bigint และ symbol ใช้ในกรณีเฉพาะทาง ค่อยเรียนเมื่อจำเป็นได้
ส่วน null กับ undefined ต่างกันตรงเจตนา:
undefined= "ตรงนี้ยังไม่ได้กำหนดค่า" (มักเกิดเองโดยอัตโนมัติ)null= "ตรงนี้จงใจให้ว่างเปล่า" (มักเป็นคนเขียนกำหนดเอง)
💡 ข้อสังเกต: ชื่อ type เป็น ตัวพิมพ์เล็ก ทั้งหมด —
stringไม่ใช่StringตัวString(พิมพ์ใหญ่) มีความหมายอื่น (เป็น object wrapper) อย่าใช้ผิด ใช้ตัวเล็กเสมอ
1.1 ส่วนที่เพิ่มเข้ามาคือ : type
typescript
let age: number = 42;
// ↑──────┘
// นี่คือ "type annotation" — ส่วนที่ TypeScript เพิ่มจาก JavaScriptโครงสร้างคือ ชื่อตัวแปร : ชนิด = ค่า — เครื่องหมาย : ตามด้วยชื่อ type คือการ "ติดป้ายบอกชนิด" ส่วนที่เหลือ (let age = 42) คือ JavaScript ปกติทุกประการ
Part 2: Type Annotation vs Type Inference — ประกาศเอง หรือให้ TS เดา
นี่เป็นแนวคิดที่มือใหม่มักสับสน ขออธิบายให้ชัด
2.1 สองวิธีในการกำหนด type
วิธีที่ 1 — Annotation (เราเขียน type เอง):
typescript
let score: number = 100;วิธีที่ 2 — Inference (TypeScript เดาเอง):
typescript
let score = 100; // เราไม่ได้เขียน : number แต่ TypeScript รู้เองว่ามันคือ numberในวิธีที่ 2 TypeScript ดูที่ค่า 100 แล้ว "เดา" (อังกฤษคือ infer) ว่าตัวแปรนี้ต้องเป็น number แน่ ๆ — และมันเดาถูก การเดานี้เรียกว่า type inference
ลองพิสูจน์ว่ามันได้ type จริง:
typescript
let score = 100;
score = "hello"; // ❌ Error! เพราะ TypeScript รู้ว่า score เป็น numberแม้เราจะไม่ได้เขียน : number แต่ TypeScript ก็ปกป้องเราเหมือนกัน
2.2 แล้วควรใช้อันไหน?
กฎง่าย ๆ: ปล่อยให้ TypeScript เดาเมื่อมันเดาได้ถูก และเขียน type เองที่ "ขอบเขต" (boundary)
"ขอบเขต" ที่ควรเขียน type เองคือ:
- พารามิเตอร์ของฟังก์ชัน — TypeScript เดาไม่ได้ว่าคนจะส่งอะไรเข้ามา
- ค่าที่ฟังก์ชันคืน (return type) — เขียนไว้เพื่อความชัดเจนและกันพลาด
- ตอนที่ค่าเริ่มต้นยังไม่มี — เช่น
let result: string;(ยังไม่กำหนดค่า)
typescript
// ✅ ดี — เขียน type ที่ param และ return (ขอบเขตของฟังก์ชัน)
function calculateTax(income: number): number {
const rate = 0.07; // ปล่อยให้เดา — เดาถูกอยู่แล้วว่าเป็น number
return income * rate;
}
// ❌ รกเกินจำเป็น — TypeScript เดาได้อยู่แล้ว ไม่ต้องเขียน
const message: string = "hello";🎯 หลักคิด: เขียน type ที่ "หน้าด่าน" ของฟังก์ชัน เพราะนั่นคือสัญญาที่คนอื่นต้องเห็น — ส่วนรายละเอียดภายในปล่อยให้ TypeScript ทำงาน
2.3 let vs const ส่งผลต่อการเดา type
มีรายละเอียดเล็ก ๆ ที่สำคัญ:
typescript
let a = "hello"; // type ที่เดาได้: string
const b = "hello"; // type ที่เดาได้: "hello" ← สังเกต! ไม่ใช่ stringทำไมต่างกัน?
let= ตัวแปรนี้ "เปลี่ยนค่าได้" — TypeScript จึงเดากว้าง ๆ ว่าเป็นstring(จะได้ใส่ string อื่นทีหลังได้)const= ตัวแปรนี้ "เปลี่ยนค่าไม่ได้ตลอดไป" — TypeScript จึงเดาแคบที่สุดเป็น"hello"เป๊ะ ๆ
type ที่เป็นค่าเป๊ะ ๆ แบบ "hello" เรียกว่า literal type (ชนิดค่าเป๊ะ — ระบุค่าตรง ๆ ไม่ใช่ชนิดกว้าง ๆ) — เดี๋ยวเราจะพูดถึงใน Part 5
Part 3: Array — เก็บข้อมูลหลายชิ้นชนิดเดียวกัน
Array (อาเรย์) คือ "กล่องที่ใส่ของหลายชิ้นเรียงต่อกัน" ใน TypeScript เราต้องบอกด้วยว่า "ของในกล่องเป็นชนิดอะไร"
3.1 การประกาศ array
มี 2 วิธีเขียนที่ความหมายเหมือนกันเป๊ะ:
typescript
let scores: number[] = [90, 85, 77]; // วิธีที่ 1 — ใช้ []
let names: Array<string> = ["A", "B"]; // วิธีที่ 2 — ใช้ Array<>ส่วนใหญ่นิยมวิธีที่ 1 (number[]) เพราะสั้นกว่า อ่านง่ายกว่า
💡 การตรวจชนิดของ array แบบนี้คือ "ทุกช่องต้องเป็นชนิดเดียวกัน" ง่ายกว่า tuple ที่ตรวจ "แต่ละตำแหน่งแยกกัน" (ดู Part 4) — อย่าเผลอเข้าใจว่า array ปกติเข้มงวดเท่า tuple
typescript
let scores: number[] = [90, 85, 77];
scores.push(100); // ✅ ใส่ number ได้
scores.push("A"); // ❌ Error! ใส่ string ในกล่องของ number ไม่ได้3.2 Array ที่เก็บได้หลายชนิด
ถ้าอยากให้ array เก็บได้หลายชนิด ใช้ | (union — เดี๋ยวอธิบายเต็มใน Part 9):
typescript
let mixed: (number | string)[] = [1, "two", 3, "four"];⚠️ สังเกตวงเล็บ (number | string)[] — ต้องมีวงเล็บครอบ ไม่งั้นความหมายเพี้ยน
3.3 Array ซ้อน Array
ข้อมูลสองมิติ (ตาราง, กริด) เก็บด้วย array ซ้อน array — เขียน type เป็น number[][] (array ที่แต่ละช่องเป็น array ของ number):
typescript
let grid: number[][] = [
[1, 2, 3],
[4, 5, 6],
]; // array ที่แต่ละช่องเป็น array ของ number อีกที (เหมือนตาราง)3.4 Readonly Array — array ที่ห้ามแก้
บางครั้งเราอยากได้ array ที่ "อ่านได้อย่างเดียว แก้ไม่ได้" เพื่อกันความผิดพลาด:
typescript
let constants: readonly number[] = [3, 14, 159];
constants.push(265); // ❌ Error! readonly array แก้ไม่ได้
constants[0] = 0; // ❌ Error! เช่นกันมีประโยชน์มากเวลาส่ง array เข้าฟังก์ชัน — บอกว่า "ฟังก์ชันนี้แค่อ่าน array ไม่แก้นะ"
Part 4: Tuple — array ที่กำหนดจำนวนและชนิดของแต่ละช่องตายตัว
4.1 ปัญหาที่ Tuple แก้
Array ปกติ ทุกช่องเป็นชนิดเดียวกัน และมีกี่ช่องก็ได้ แต่บางครั้งเราต้องการ "array ที่มีจำนวนช่องแน่นอน และแต่ละช่องชนิดต่างกัน"
ตัวอย่าง: เราอยากเก็บ "พิกัด" เป็นคู่ของ [ชื่อแกน, ค่า] — ช่องแรกต้องเป็น string ช่องที่สองต้องเป็น number มีแค่ 2 ช่องเท่านั้น
นี่คืองานของ Tuple (อ่านว่า ทู-เพิ่ล):
typescript
let pair: [string, number] = ["age", 25];
pair[0]; // TypeScript รู้ว่าช่องนี้เป็น string
pair[1]; // TypeScript รู้ว่าช่องนี้เป็น number
pair[2]; // ❌ Error! tuple นี้มีแค่ 2 ช่อง
let wrong: [string, number] = [25, "age"]; // ❌ Error! สลับชนิดผิดช่อง4.2 Tuple แบบมีชื่อช่อง (อ่านง่ายขึ้น)
เราตั้งชื่อให้แต่ละช่องได้ เพื่อให้คนอ่านเข้าใจ (ชื่อนี้มีผลแค่ช่วยอ่าน ไม่มีผลตอน runtime):
typescript
let user: [name: string, age: number] = ["Alice", 30];4.3 Tuple แบบมีช่องไม่บังคับ และช่องที่เหลือ
typescript
// ช่องสุดท้ายไม่บังคับ (ใส่ ? )
let point: [number, number, number?] = [10, 20]; // ช่องที่ 3 จะมีหรือไม่มีก็ได้
// point[2] จะมี type เป็น number | undefined — ต้องเช็กก่อนใช้ เหมือน optional property ของ object
// ช่องที่เหลือเป็นจำนวนเท่าไรก็ได้ (ใช้ ... )
let row: [string, ...number[]] = ["total", 1, 2, 3, 4];
// ช่องแรก string ช่องที่เหลือเป็น number กี่ตัวก็ได้📌 เมื่อไหร่ใช้ Tuple? เมื่อข้อมูลมี "โครงสร้างตายตัว" เช่น คู่ลำดับ พิกัด หรือค่าที่ React hook คืน (
const [count, setCount] = useState(0)— นั่นแหละ tuple!)
Part 5: ชนิดค่าเป๊ะ (Literal Types) — type ที่เป็นค่าเป๊ะ ๆ
5.1 แนวคิด
ปกติ type string หมายถึง "ข้อความอะไรก็ได้" แต่ literal type คือการระบุว่า "ต้องเป็น ค่านี้เป๊ะ ๆ เท่านั้น":
📌 หมายเหตุก่อนดูตัวอย่าง: ตัวอย่างข้างล่างนี้เขียนเป็นตัวแปรเดี่ยว ๆ เพื่อให้เห็น syntax ชัด ๆ ก่อน — ในโค้ดจริงแทบไม่มีใครประกาศตัวแปรแบบนี้ตรง ๆ, pattern นี้จะไปโผล่ใน return type หรือ property ของ object แทน (ดูตัวอย่างจริงในหัวข้อถัดไป)
typescript
let answer: "yes"; // ประกาศ type ก่อน — ตัวแปรนี้รับได้แค่ "yes"
answer = "yes"; // ✅
answer = "no"; // ❌ Error!literal type ตัวเดียวอาจดูไร้ประโยชน์ (จะมีตัวแปรที่เก็บได้ค่าเดียวไปทำไม) — แต่พลังของมันจะปรากฏเมื่อเอามารวมกับ union
5.2 Union ของ Literal — "string enum" ที่แท้จริง
typescript
let role: "admin" | "editor" | "viewer";
role = "admin"; // ✅
role = "editor"; // ✅
role = "superuser"; // ❌ Error! ไม่ได้อยู่ในรายการที่อนุญาตนี่มีประโยชน์มหาศาล ลองดูตัวอย่างจริง:
typescript
type LogLevel = "debug" | "info" | "warn" | "error";
function log(level: LogLevel, message: string) {
console.log(`[${level}] ${message}`);
}
log("info", "เริ่มทำงาน"); // ✅
log("INFO", "เริ่มทำงาน"); // ❌ Error! พิมพ์ใหญ่ผิด TypeScript จับได้
log("verbose", "..."); // ❌ Error! ไม่มี level นี้ข้อดี: คนเรียกใช้ฟังก์ชันนี้จะ ใส่ค่าผิดไม่ได้เลย และ IDE จะ autocomplete 4 ตัวเลือกให้ด้วย — ไม่ต้องเดา ไม่ต้องเปิดเอกสาร
literal type ใช้ได้กับตัวเลขและ boolean ด้วย:
typescript
let httpStatus: 200 | 404 | 500;
let mustBeTrue: true;Part 6: Enum — ฟีเจอร์ที่มี trade-off (ข้อแลกเปลี่ยน) จนปัจจุบันมักเลี่ยง
enum (อ่านว่า อี-นัม) เป็นอีกวิธีในการสร้าง "ชุดค่าที่กำหนดไว้" คุณจะเจอมันในโค้ดเก่า จึงต้องรู้จัก — TypeScript ยังรองรับเต็มที่ ไม่ได้ deprecate แต่ในโปรเจกต์ใหม่ปี 2026 ส่วนมากเลือกใช้ทางอื่นแทน (เพราะ trade-off หรือข้อแลกเปลี่ยนที่จะเล่าใน 6.2)
6.1 หน้าตาของ enum
มาดูหน้าตาของ enum กัน — ประกาศด้วยคีย์เวิร์ด enum ตามด้วยรายชื่อสมาชิก ถ้าไม่กำหนดค่า สมาชิกจะได้ตัวเลขเริ่มจาก 0 อัตโนมัติ:
typescript
enum Direction {
Up, // ได้ค่า 0 โดยอัตโนมัติ
Down, // ได้ค่า 1
Left, // ได้ค่า 2
Right, // ได้ค่า 3
}
let move: Direction = Direction.Up;
// แบบกำหนดค่าเป็น string เอง
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
}6.2 ทำไมปัจจุบันมักเลี่ยง enum
จำเรื่อง type erasure จากบทที่ 00 ได้ไหม — type ส่วนใหญ่หายไปตอน compile แต่ enum ไม่หาย มันสร้างโค้ด JavaScript จริง ๆ ออกมา ทำให้:
- ขนาดไฟล์ใหญ่ขึ้นโดยไม่จำเป็น
- พฤติกรรมบางอย่างชวนงง (โดยเฉพาะ enum ตัวเลข)
- เครื่องมือ build สมัยใหม่บางตัว (เช่น esbuild) ไม่สามารถ inline
const enumข้ามไฟล์ได้ ทำให้ build ล้มเหลวเมื่อ importconst enumจากไฟล์อื่น — พฤติกรรมนี้ขึ้นอยู่กับ bundler/เวอร์ชันที่ใช้ ควรตรวจสอบเอกสารของเครื่องมือที่ใช้จริงก่อนพึ่งพาconst enumข้ามไฟล์
6.3 ใช้อะไรแทน
ทางเลือกที่ 1 — union ของ literal (ง่ายสุด ใช้บ่อยสุด):
typescript
type Status = "ACTIVE" | "INACTIVE";ทางเลือกที่ 2 — object ปกติ + as const (เมื่ออยากมีตัวแปรจริง ๆ ไว้อ้างอิง):
typescript
const Status = {
Active: "ACTIVE",
Inactive: "INACTIVE",
} as const;ใช้แค่นี้ก็เพียงพอสำหรับกรณีส่วนใหญ่ — คุณอ้างถึงค่าผ่าน Status.Active ได้ทันที
ขั้นสูง — ดึง union type ออกจาก const object (ยากเกินจำเป็นตอนนี้)
ถ้าอยากให้มี type Status ด้วย (เพื่อใช้เป็น type annotation) ทำได้แต่ต้องใช้ syntax ขั้นสูง:
typescript
type Status = typeof Status[keyof typeof Status];
// type Status ที่ได้คือ "ACTIVE" | "INACTIVE"💡 ถ้าสงสัยว่าบรรทัดนี้แปลว่าอะไร:
typeof Status[keyof typeof Status]แปลตรงตัวได้ว่า "ชนิดของค่าทั้งหมดใน Status object" — ฟีเจอร์keyofและtypeofในตำแหน่ง type จะอธิบายเต็ม ๆ ใน บทที่ 05 ตอนนี้ถ้ายังไม่เข้าใจไม่เป็นไร — แค่ก๊อปไปใช้ได้เลย
ทำไมตั้งชื่อ Status ซ้ำได้? สรุปสั้น ๆ ก่อน: TypeScript แยกเก็บชื่อ const กับชื่อ type คนละที่กัน จึงตั้งชื่อซ้ำกันได้โดยไม่ชนกัน — อ่านรายละเอียดว่าทำไมถึงเป็นแบบนั้นด้านล่าง
สังเกตว่าเราตั้งชื่อ Status ซ้ำกัน 2 ที่ — const Status กับ type Status — TypeScript ยอมเพราะใน TypeScript มี 2 โลกแยกกัน เก็บชื่อ:
- value space — เก็บชื่อตัวแปร/ฟังก์ชัน/class ที่อยู่ตอน runtime
- type space — เก็บชื่อ type/interface ที่หายไปตอน compile
const Status = {...} อยู่ใน value space ส่วน type Status = ... อยู่ใน type space ทั้งสองไม่ชนกันเพราะอยู่คนละโลก — TypeScript รู้จากบริบทเองว่าจะหยิบจากที่ไหน:
typescript
let s: Status = Status.Active;
// ↑ ↑
// type space value space
// (ดึง type) (ดึงค่า)นี่คือ pattern ที่นิยมมาก เพราะได้ทั้ง "ค่าจริงไว้ใช้ runtime" และ "type ไว้ใช้ตรวจเช็ก" ใต้ชื่อเดียวกัน
⚠️ ระวังตอน import: เพราะอยู่คนละโลก
import { Status }จะดึงมาแค่ value เท่านั้น (object จริง ๆ) ถ้าอยาก import ทั้งคู่ ใน TypeScript 5.7 ให้เขียนแบบนี้:typescript// ดึง value Status + type Status พร้อมกัน import { Status } from "./status"; // ↑ ดึง value มา; type ที่ชื่อเดียวกันจะมาด้วยอัตโนมัติ // (ทำงานได้เพราะมันถูก declare คู่กันในไฟล์ต้นทาง) // ถ้าเปิด verbatimModuleSyntax และอยาก import เฉพาะ type import type { Status } from "./status";
📌 สรุป: เจอ
enumในโค้ดเก่า — เข้าใจมันได้ เขียนโค้ดใหม่ — ใช้ union literal type แทน
Part 7: สามทหารเสือพิเศษ — any, unknown, never
3 ตัวนี้คือ type ที่ "ไม่ธรรมดา" และมือใหม่มักใช้ผิด โดยเฉพาะ any ตั้งใจอ่านส่วนนี้ให้ดี
7.1 any — "ปิดสวิตช์ TypeScript"
any แปลว่า "อะไรก็ได้ และฉันไม่อยากให้ TypeScript ตรวจสอบมัน"
typescript
let data: any = "hello";
data = 42; // ได้
data = true; // ได้
data = { userId: 42 }; // ได้
data.userId.profile.email(); // ได้!! TypeScript ไม่ฟ้องอะไรเลย
data(); // ได้!! ทั้งที่ data ไม่ใช่ฟังก์ชันเห็นไหมว่าอันตรายแค่ไหน — เมื่อค่าเป็น any TypeScript จะ "หลับตา" ปล่อยให้คุณทำอะไรกับมันก็ได้ทั้งหมด ซึ่งหมายความว่าบั๊กที่ควรถูกจับ จะหลุดไปพังตอน runtime แทน
⚠️ เปรียบเทียบ: การใช้
anyก็เหมือนการถอดเข็มขัดนิรภัยออกเพราะมันรำคาญ — สบายขึ้นชั่วคราว แต่คุณทิ้งการปกป้องทั้งหมดไป
กฎ: หลีกเลี่ยง any ให้มากที่สุด ถ้าคุณเขียน any บ่อย ๆ แปลว่าคุณกำลังเสียประโยชน์ของ TypeScript ไปฟรี ๆ ถ้าคุณ "ไม่รู้ว่า type คืออะไร" — ให้ใช้ unknown แทน (อ่านต่อ)
7.2 unknown — "อะไรก็ได้ แต่ปลอดภัย"
unknown ก็แปลว่า "อะไรก็ได้" เหมือน any — แต่ต่างกันที่จุดสำคัญมาก
typescript
let value: unknown = "hello";
value = 42; // ✅ ใส่ค่าอะไรก็ได้ เหมือน any
value = { x: 1 }; // ✅
value.toUpperCase(); // ❌ Error! TypeScript ไม่ยอมให้ทำอะไรกับ unknown
value.foo; // ❌ Error!ความต่างคือ — any ยอมให้คุณ ทำอะไรกับมันก็ได้ แต่ unknown ห้ามทำอะไรกับมันเลย จนกว่าคุณจะ "พิสูจน์" ก่อนว่ามันคือ type อะไร:
typescript
let value: unknown = getDataFromSomewhere();
if (typeof value === "string") {
// ในบล็อกนี้ TypeScript รู้แล้วว่า value เป็น string
value.toUpperCase(); // ✅ ตอนนี้ทำได้แล้ว
}การ "พิสูจน์ type ก่อนใช้" แบบนี้เรียกว่า narrowing (การทำให้ type แคบลง/ระบุชัด — บทที่ 07 จะลงลึก)
🎯 หลักการ: ใช้
unknownแทนanyแทบทุกกรณี — มันคือ "อะไรก็ได้แบบรับผิดชอบ" ที่บังคับให้คุณตรวจสอบก่อนใช้ เหมาะมากสำหรับข้อมูลจากภายนอก (API, JSON.parse) ส่วนanyยังมีที่ใช้ตรงจริง ๆ อยู่บ้าง เช่น migrate โค้ด JavaScript เก่าแบบค่อยเป็นค่อยไป แต่ไม่ควรใช้เป็นทางลัดทั่วไป
7.3 never — "type ที่เป็นไปไม่ได้"
never เป็นแนวคิดที่แปลกที่สุด มันหมายถึง "ค่าที่ไม่มีวันเกิดขึ้น"
ฟังดูไร้ประโยชน์ แต่จริง ๆ มีที่ใช้ 2 แบบ:
แบบที่ 1 — ฟังก์ชันที่ "ไม่มีวันคืนค่า"
typescript
// ฟังก์ชันที่โยน error เสมอ — มันไม่มีวันรันถึงบรรทัด return
function throwError(message: string): never {
throw new Error(message);
}
// ฟังก์ชันที่วนลูปไม่มีวันจบ
function loopForever(): never {
while (true) {
// ...
}
}ฟังก์ชันพวกนี้ "ไม่มีวันคืนค่าปกติ" — return type จึงเป็น never
แบบที่ 2 — "exhaustive check" (ตรวจครบทุกกรณี ไม่มีหลุด) ตรวจว่าเราจัดการครบทุกกรณี (อันนี้มีประโยชน์มาก)
typescript
type Shape = "circle" | "square" | "triangle";
function getSides(shape: Shape): number {
switch (shape) {
case "circle": return 0;
case "square": return 4;
case "triangle": return 3;
default:
// ถ้าจัดการครบทุก case แล้ว มาถึงตรงนี้ shape จะมี type เป็น never
const _exhaustive: never = shape;
return _exhaustive;
}
}อ่านโค้ดนี้ทีละขั้น:
switch (shape) { ... }— ตรวจค่าของshapeแล้วกระโดดไปทำcaseที่ตรง- แต่ละ
caseเป็น literal type หนึ่งตัวจาก unionShape— ครอบคลุมครบ 3 ค่า default:คือ "ถ้าไม่ตรงทุก case ข้างบน ให้ทำตรงนี้" — ในทางทฤษฎี ถ้าเราจัดการครบแล้ว ตรงนี้จะไม่มีวันรันถึง- ใน
default:เราประกาศตัวแปร_exhaustivetypeneverแล้วยัดshapeใส่ — นี่คือกลไกสำคัญ
ทำไมยัด shape เข้า never ถึงทำงานเป็น check ได้? ไล่ทีละขั้น:
- TypeScript "narrow" (ทำให้แคบลง) type ของ
shapeทีละcaseที่ผ่าน - พอผ่านครบทั้ง 3
caseโดยแต่ละอันมีreturnจบไป — ไม่มี case ไหนเหลือให้ตรวจอีก - ดังนั้นในบรรทัด
default:TypeScript สรุปว่าshape"ใช้หมด" ไปแล้ว เหลือ type เป็นneverพอดี - การยัดค่า type
neverลงตัวแปรที่ประกาศ type เป็นneverจึงผ่าน (เพราะชนิดตรงกัน)
ประโยชน์: ถ้าวันหนึ่งมีคนเพิ่ม "hexagon" เข้าไปใน Shape แต่ลืมเพิ่ม case ในฟังก์ชันนี้ — ตอนมาถึง default: TypeScript จะเห็นว่า shape ยังเป็น "hexagon" ที่ยังไม่ถูกจัดการ (ไม่ใช่ never) บรรทัด const _exhaustive: never = shape จะ error ทันที บอกเราตอน compile ว่า "ลืมจัดการ hexagon"
นี่คือเทคนิคที่ทำให้ TypeScript "บังคับให้เราจัดการทุกกรณี" — เราจะใช้บ่อยมากในบทที่ 07 และ 10
Part 8: void — ฟังก์ชันที่ไม่คืนค่า
void ใช้เป็น return type ของฟังก์ชันที่ "ทำงานบางอย่าง แต่ไม่คืนค่ากลับมา"
typescript
function logMessage(message: string): void {
console.log(message);
// ไม่มี return ที่มีค่า
}ฟังก์ชันที่ทำหน้าที่ "ก่อผลข้างเคียง" (เช่น พิมพ์ออกจอ, บันทึกไฟล์, ส่ง request) มักมี return type เป็น void
void ต่างจาก undefined ดังนี้:
typescript
// void — คุณโยนผลลัพธ์ทิ้งได้เลย TypeScript ไม่ฟ้อง
function doSomething(): void {
console.log("done");
// return โดยไม่มีค่า หรือไม่มี return เลยก็ได้
}
// undefined — ต้อง return undefined จริง ๆ
function returnUndefined(): undefined {
return undefined; // ต้องมีบรรทัดนี้ ถ้าไม่มีจะ error
}ในทางปฏิบัติมือใหม่แค่จำว่า "ฟังก์ชันไม่คืนค่า = void" ก็พอ ส่วน undefined ใช้เมื่ออยากบังคับชัด ๆ ว่าต้อง return undefined
Part 9: ประกอบ Type เข้าด้วยกัน — Union (รวม) กับ Intersection (ผสาน)
ตอนนี้เรารู้จัก type พื้นฐานแล้ว ต่อไปคือการ "เอา type มาประกอบกัน" เป็น type ใหม่ มี 2 วิธีหลัก
9.1 Union (|) — "เป็นอันใดอันหนึ่ง"
Union อ่านว่า "หรือ" — ใช้เครื่องหมาย | หมายถึง "ค่านี้เป็น type A หรือ type B ก็ได้"
typescript
let id: string | number;
id = "abc123"; // ✅ เป็น string ได้
id = 42; // ✅ เป็น number ได้
id = true; // ❌ Error! ไม่ได้เป็นทั้ง string หรือ numberใช้บ่อยมากในชีวิตจริง เช่น ฟังก์ชันที่รับ id ได้ทั้งแบบตัวเลขและข้อความ:
typescript
function findUser(id: string | number) {
// ...
}
findUser(1);
findUser("u-001");⚠️ ข้อควรระวังของ union: เมื่อค่าเป็น union คุณจะทำได้แค่สิ่งที่ "ทุก type ในนั้นทำได้ร่วมกัน":
typescript
function printId(id: string | number) {
id.toUpperCase(); // ❌ Error! number ไม่มี toUpperCase
// ต้อง narrow ก่อน:
if (typeof id === "string") {
id.toUpperCase(); // ✅ ในนี้ id เป็น string แน่นอน
}
}(เรื่อง narrowing จะลงลึกในบทที่ 07)
9.2 Intersection (&) — "เป็นทั้งสองอย่างพร้อมกัน"
Intersection อ่านว่า "และ" — ใช้เครื่องหมาย & หมายถึง "ค่านี้ต้องเป็นทั้ง type A และ type B รวมกัน"
typescript
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;
// Person ต้องมีทั้ง name และ age
let p: Person = { name: "Alice", age: 30 }; // ✅ มีครบทั้งคู่
let q: Person = { name: "Bob" }; // ❌ Error! ขาด age& มีประโยชน์ในการ "รวม" หลาย type เข้าด้วยกันเป็น type ใหญ่
9.3 จำง่าย ๆ
- Union
|= "หรือ" = ได้ type ที่ ยืดหยุ่นกว่า (เป็นอันใดอันหนึ่ง) — ใช้กับ "ค่า" - Intersection
&= "และ" = ได้ type ที่ เข้มงวดกว่า (ต้องครบทุกอย่าง) — ใช้รวม "object types"
💡 ฟังดูสวนสามัญสำนึก —
&(และ) ทำให้ object ใหญ่ขึ้นแต่เงื่อนไขเข้มขึ้น ส่วน|(หรือ) ทำให้ค่ายืดหยุ่นขึ้น ลองคิดในแง่ "เซตของค่าที่เป็นไปได้" —|ขยายเซต,&หดเซต
Part 10: Object Type เบื้องต้น
ตอนนี้รู้จัก type ของค่าเดี่ยว ๆ แล้ว มาดูการอธิบาย "รูปร่างของ object"
typescript
// อธิบาย object โดยตรง
let user: { name: string; age: number } = {
name: "Alice",
age: 25,
};แต่การเขียนแบบนี้ซ้ำ ๆ ไม่สะดวก เราจึง "ตั้งชื่อ" ให้ type ได้ 2 วิธี:
typescript
// วิธีที่ 1 — type alias (ชื่อเล่นของ type — ตั้งชื่อให้ type ใช้ซ้ำได้) ใช้คำว่า type
type User = {
name: string;
age: number;
email?: string; // ? = field ไม่บังคับ มีหรือไม่มีก็ได้
readonly id: number; // readonly = กำหนดครั้งเดียว แก้ไม่ได้อีก
};
// วิธีที่ 2 — interface (สอนเต็ม ๆ ในบทที่ 04) ใช้คำว่า interface
interface User {
name: string;
age: number;
}ทั้งสองวิธีใช้แทนกันได้เกือบหมด รายละเอียดเชิงลึกของ object type, interface, optional, readonly อยู่ใน บทที่ 04 เต็ม ๆ ในบทนี้แค่รู้จักหน้าตาก่อน
10.1 type vs interface — เลือกอันไหน
มือใหม่มักสงสัยว่าใช้อันไหนดี ตารางนี้สรุปความต่าง:
| ความสามารถ | type | interface |
|---|---|---|
| อธิบาย object | ✅ | ✅ |
ใช้กับ union (A | B) | ✅ | ❌ |
| ตั้งชื่อให้ primitive/tuple | ✅ | ❌ |
| ขยายจากตัวอื่น | ✅ ใช้ & (intersection) | ✅ ใช้ extends |
| declaration merging (การประกาศซ้ำชื่อเดิมแล้ว TypeScript รวมให้อัตโนมัติ) | ❌ | ✅ |
คำแนะนำง่าย ๆ สำหรับมือใหม่: ตอนนี้ยังไม่ต้องคิดมาก — ใช้ type เป็นหลักก็ได้ เพราะมันทำได้ทุกอย่าง (union, tuple ฯลฯ) ส่วน interface ค่อยใช้เมื่อเขียน library สาธารณะ เดี๋ยวบทที่ 04 อธิบายละเอียด
Part 11: การยืนยันชนิด (Type Assertion) — "เชื่อฉันเถอะ TypeScript"
บางครั้งคุณ "รู้" type ของค่ามากกว่าที่ TypeScript รู้ คุณสามารถ "ยืนยัน" type ให้ TypeScript ได้ด้วยคำว่า as:
typescript
const input = document.getElementById("username") as HTMLInputElement;
input.value = "hello"; // ตอนนี้ TypeScript รู้ว่ามันเป็น input elementปกติ getElementById คืน type กว้าง ๆ (HTMLElement | null) ที่ไม่มี property .value แต่เรา (คนเขียน) รู้ว่ามันคือ <input> แน่ ๆ จึงใช้ as บอก TypeScript
⚠️ อันตราย:
asคือการพูดว่า "ฉันรู้ดีกว่าเธอ TypeScript" — ถ้าคุณรู้ผิด TypeScript จะไม่ช่วยอะไรเลย และโปรแกรมจะพังตอน runtimetypescriptconst x = "hello" as unknown as number; // TypeScript เชื่อ แต่ x ไม่ใช่ number จริง x.toFixed(2); // 💥 พังตอน runtimeใช้
asให้น้อยที่สุด ถ้าเป็นข้อมูลจากภายนอก ให้ตรวจสอบจริง ๆ (เช่น Zod) ดีกว่าใช้as
11.1 as const — แช่แข็งค่าให้เป็น literal
as const เป็น assertion ชนิดพิเศษที่มีประโยชน์มากและไม่อันตราย มันบอก TypeScript ว่า "ค่านี้จะไม่เปลี่ยนเลย เดา type ให้แคบที่สุด":
typescript
// ไม่มี as const
const colors1 = ["red", "green", "blue"];
// type: string[] ← กว้าง
// มี as const
const colors2 = ["red", "green", "blue"] as const;
// type: readonly ["red", "green", "blue"] ← แคบและตายตัว
// ประโยชน์ — ดึง type ออกมาได้
type Color = typeof colors2[number];
// type Color = "red" | "green" | "blue"📌 ส่วน
[number]ที่ดูแปลก เรียกว่า indexed access type (เข้าถึง type ผ่าน index) — บอกความว่า "เอา type ของสมาชิกใน array tuple นี้มาทุกตัวรวมกันเป็น union" รายละเอียดเต็มอยู่ในบทขั้นสูง (บทที่ 06 และ 10) ตอนนี้แค่ก๊อปไปใช้เป็น pattern พอ
as const ใช้บ่อยมากในการสร้างชุดค่าคงที่ (เราเห็นไปแล้วใน Part 6.3)
11.2 Non-null Assertion (!)
เครื่องหมาย ! ต่อท้ายค่า เป็นการบอกว่า "ค่านี้ไม่ใช่ null/undefined แน่นอน เชื่อฉัน":
typescript
const el = document.getElementById("app")!;
// ↑ บอกว่า "ไม่ null แน่"
el.innerHTML = "hello";⚠️ อันตรายแบบเดียวกับ as — ถ้าจริง ๆ มันเป็น null โปรแกรมจะพัง ใช้เมื่อมั่นใจจริง ๆ เท่านั้น ถ้าไม่มั่นใจ ให้เช็กด้วย if ดีกว่า
Part 12: Index Signature — object ที่ key ไม่รู้ล่วงหน้า
บางครั้งเรามี object ที่ "ไม่รู้ว่าจะมี key อะไรบ้าง" รู้แค่ว่า "key เป็น string และ value เป็น number" เช่น ตารางคะแนน:
typescript
type ScoreBoard = {
[playerName: string]: number;
// ↑ key เป็น string อะไรก็ได้ ↑ value เป็น number
};
const scores: ScoreBoard = {
alice: 90,
bob: 85,
charlie: 77,
};
scores.david = 60; // ✅ เพิ่ม key ใหม่ได้ส่วน [playerName: string]: number เรียกว่า index signature (ลายเซ็นดัชนี — บอกหน้าตา key/value แบบไม่ต้องระบุชื่อ key ทีละตัว)
วิธีที่อ่านง่ายกว่าและนิยมกว่าคือใช้ Record (utility type — บทที่ 06):
typescript
type ScoreBoard = Record<string, number>;
// ความหมายเหมือนข้างบนเป๊ะ แต่อ่านง่ายกว่าPart 13: Lab — ลงมือทำ
ลองทำใน TypeScript Playground
Lab 1 — ออกแบบ type ของผู้ใช้
typescript
type AccountStatus = "pending" | "active" | "suspended";
type User = {
readonly id: number;
username: string;
email: string;
age?: number; // ไม่บังคับ
status: AccountStatus;
roles: readonly string[]; // อ่านอย่างเดียว
address: {
city: string;
country: "TH" | "JP" | "US";
};
};
const user: User = {
id: 1,
username: "alice",
email: "alice@example.com",
status: "active",
roles: ["member"],
address: { city: "Bangkok", country: "TH" },
};ลองทำให้เกิด error ดู: เปลี่ยน status: "active" เป็น "deleted", หรือ country: "TH" เป็น "FR" — TypeScript ควรฟ้องทั้งคู่
Lab 2 — Tuple
ฝึกใช้ tuple (array ที่กำหนดชนิดและจำนวนสมาชิกตายตัว) เป็น return type — คืนค่าหลายตัวพร้อมตั้งชื่อให้แต่ละตำแหน่ง แล้วแกะด้วย destructuring:
typescript
function getMinMax(numbers: number[]): [min: number, max: number] {
return [Math.min(...numbers), Math.max(...numbers)];
}
const [low, high] = getMinMax([5, 1, 9, 3]);
console.log(`ต่ำสุด ${low}, สูงสุด ${high}`);Lab 3 — Union แบบ discriminated (เกริ่นนำบทที่ 07)
typescript
type ApiResult =
| { success: true; data: string }
| { success: false; error: string };
function handleResult(result: ApiResult): string {
if (result.success) {
return `ได้ข้อมูล: ${result.data}`;
} else {
return `ผิดพลาด: ${result.error}`;
}
}Part 14: Checkpoint
1. let x = 5 กับ const x = 5 TypeScript เดา type เป็นอะไร ต่างกันยังไง?
let x = 5 เดาเป็น number (เพราะ let เปลี่ยนค่าได้) ส่วน const x = 5 เดาเป็น 5 ซึ่งเป็น literal type (เพราะ const เปลี่ยนค่าไม่ได้ จึงเดาแคบที่สุด)
2. any กับ unknown ต่างกันยังไง ควรใช้อันไหน?
any ปิดการตรวจสอบทั้งหมด — ทำอะไรกับมันก็ได้ (อันตราย) unknown ก็รับค่าอะไรก็ได้ แต่ ห้ามใช้งานจนกว่าจะ narrow ก่อน — ควรใช้ unknown แทน any เสมอ
3. never ใช้ตอนไหน?
(1) เป็น return type ของฟังก์ชันที่ไม่มีวันคืนค่า (โยน error เสมอ หรือวนลูปไม่จบ) (2) ใช้ทำ exhaustive check เพื่อบังคับให้จัดการครบทุกกรณีใน union
4. Tuple ต่างจาก Array ปกติยังไง?
Array ปกติ ทุกช่องชนิดเดียวกันและมีกี่ช่องก็ได้ ส่วน Tuple มีจำนวนช่องแน่นอนและแต่ละช่องระบุชนิดเฉพาะของมัน
5. ทำไมปี 2026 มักเลี่ยง enum?
เพราะ enum ไม่ถูกลบตอน compile (สร้างโค้ด JS จริง ทำให้ไฟล์ใหญ่ขึ้น) พฤติกรรมบางอย่างชวนงง และ const enum จะ error เมื่อใช้ข้ามไฟล์กับ isolatedModules: true — ใช้ union literal type แทน
6. Union (|) กับ Intersection (&) ต่างกันยังไง?
| = "เป็นอันใดอันหนึ่ง" (ยืดหยุ่นกว่า, ใช้กับค่า) & = "เป็นทั้งสองอย่างพร้อมกัน" (เข้มงวดกว่า, ใช้รวม object type)
7. Type assertion (as) อันตรายตรงไหน?
มันบอก TypeScript ให้ "เชื่อเรา" โดยไม่ตรวจสอบจริง — ถ้าเราระบุ type ผิด TypeScript จะไม่ช่วย และโปรแกรมจะพังตอน runtime
8. as const ทำอะไร?
บอก TypeScript ว่าค่านี้จะไม่เปลี่ยน ให้เดา type แคบที่สุด (เป็น literal type และ readonly) มีประโยชน์ในการสร้างชุดค่าคงที่
Part 15: สรุปบทนี้
- Primitive —
number,string,booleanใช้บ่อยสุด - Inference — ปล่อยให้ TypeScript เดา type, เขียนเองที่ "ขอบเขต" (param/return)
- Array —
T[], Tuple —[T1, T2]จำนวนและชนิดตายตัว - Literal type — type ที่เป็นค่าเป๊ะ ๆ, รวมกับ union เป็น "string enum"
- เลี่ยง
enum— ใช้ union literal แทน unknownดีกว่าanyแทบทุกกรณี เมื่อยังไม่รู้ชนิดข้อมูลล่วงหน้า (ยกเว้นกรณีพิเศษ เช่น migrate โค้ด JS เก่าแบบค่อยเป็นค่อยไป),never= type ที่เป็นไปไม่ได้ + exhaustive check- Union
|= หรือ, Intersection&= และ asอันตราย ใช้เท่าที่จำเป็น,as constปลอดภัยและมีประโยชน์
บทถัดไป — ลงลึกเรื่อง Function