TypeScript Data Model
Day 2 - ชั่วโมงที่ 3: เปลี่ยน Static Rows ให้สร้างจากข้อมูล
เป้าหมายของชั่วโมงนี้
หลังจบชั่วโมงนี้ ผู้เรียนจะสามารถ:
- อธิบายได้ว่าทำไมเราควรแยกข้อมูลออกจาก JSX
- สร้าง Type สำหรับข้อมูล Issue ได้
- ใช้ Union Type จำกัดค่าของ Status ได้
- สร้าง Mock Data แบบ
Issue[]ได้ - ใช้
.map()สร้าง Table Rows จากข้อมูลได้ - แสดง Status Badge ตามค่าที่อยู่ในข้อมูลได้
ไฟล์ที่แก้
Hour นี้แก้เพียงไฟล์เดียว:
app/page.tsxเราจะวาง Type, Mock Data และ Function ช่วยไว้ในไฟล์นี้ก่อน แล้วค่อยแยกเป็นไฟล์จริงใน Hour 4
โครงสร้างเวลา 60 นาที
| เวลา | หัวข้อ | รูปแบบ |
|---|---|---|
| 0-10 นาที | จาก Static Rows สู่ข้อมูลแบบ Array | อธิบายและเปรียบเทียบ |
| 10-25 นาที | สร้าง IssueStatus และ Issue | ทำทีละขั้นตอน |
| 25-40 นาที | สร้าง Mock Data Issue[] | ทำทีละขั้นตอน |
| 40-52 นาที | แสดง Rows ด้วย .map() | ทำทีละขั้นตอน |
| 52-60 นาที | คืน Status Badge และตรวจผลลัพธ์ | ตรวจความพร้อม |
Slide 1: จาก Static Rows ไปสู่ข้อมูล
Hour 2 เราสร้าง Table ที่มีข้อมูลเขียนอยู่ใน JSX โดยตรง:
<tr>
<td>#001</td>
<td>Login เข้าระบบไม่ได้</td>
<td>Anan</td>
<td><span className="status status-open">OPEN</span></td>
</tr>วิธีนี้แสดงผลได้ แต่ข้อมูลกับหน้าตาของ Table ปนกันอยู่ หากต้องเพิ่มหรือแก้รายการ เราต้องแก้ <tr> ทีละแถว
Hour นี้เราจะย้ายข้อมูล Issue ออกมาไว้ใน Array แล้วให้ JSX สร้าง Rows จากข้อมูลนั้น
Slide 2: หน้าตาเดิม แต่เปลี่ยนที่มาของข้อมูล
หลังปรับแล้ว Flow จะเป็น:
issues array → .map() → <tr> หลายแถวissuesเก็บข้อมูลปัญหาทั้งหมด.map()อ่านข้อมูลทีละรายการ- แต่ละรายการถูกเปลี่ยนเป็น
<tr>หนึ่งแถว
Table ที่ผู้ใช้เห็นยังเหมือนเดิม แต่เราแก้ข้อมูลได้จาก Array เพียงจุดเดียว และเตรียมพร้อมสำหรับข้อมูลจาก Database ในภายหลัง
Slide 3: TypeScript ช่วยตรวจชนิดของข้อมูล
TypeScript ทำให้เราระบุได้ว่าค่าแต่ละตัวควรเป็นชนิดใด:
const title: string = "Login เข้าระบบไม่ได้";
const total: number = 3;ถ้าใส่ค่าผิดชนิด TypeScript จะแจ้งเตือนตั้งแต่ตอนเขียน Code:
const total: number = "สาม";ในตัวอย่างนี้ total ต้องเป็น number แต่ค่าที่ใส่เป็น string เราจึงเห็นปัญหาก่อนเปิดหน้าเว็บ
Slide 4: Issue หนึ่งรายการมีข้อมูลอะไรบ้าง
ข้อมูลบางส่วนมาจาก Form และบางส่วนระบบเป็นผู้กำหนด:
| มาจาก Form | ระบบกำหนดหรืออัปเดต |
|---|---|
reporterName | id |
reporterEmail | status |
title | createdAt |
description | adminComment |
เราจะสร้าง Type ชื่อ Issue เพื่อกำหนดว่า Issue ทุกตัวต้องมี Field เหล่านี้ในรูปแบบเดียวกัน
Slide 5: จำกัด Status ด้วย Union Type
ระบบของเรารองรับ Status เพียงสามค่า:
สร้าง Union Type ใหม่ชื่อ IssueStatus ไว้ด้านบนของ app/page.tsx ก่อน export default function Home():
type IssueStatus = "OPEN" | "IN_PROGRESS" | "DONE";เครื่องหมาย | อ่านว่า “หรือ” ดังนั้น IssueStatus จึงรับได้เฉพาะ:
"OPEN""IN_PROGRESS""DONE"
หากเขียนค่าที่ไม่อยู่ในรายการ TypeScript จะแจ้งเตือน:
const status: IssueStatus = "FINISHED";Union Type เหมาะกับข้อมูลที่มีตัวเลือกจำกัด เพราะช่วยป้องกันค่าที่สะกดผิดหรือระบบไม่รองรับ
Slide 6: สร้าง Type Issue
Issue คือ Type ที่อธิบายว่า ข้อมูลปัญหาหนึ่งรายการต้องมีหน้าตาอย่างไร เช่น ต้องมี id, title, status และแต่ละ Field ต้องเก็บข้อมูลชนิดใด
เราสร้าง Type นี้เพื่อให้ Issue ทุกตัวใช้โครงสร้างเดียวกัน หาก Object ใดลืม Field สำคัญ ใส่ข้อมูลผิดชนิด หรือใช้ Status ที่ระบบไม่รองรับ TypeScript จะช่วยแจ้งเตือน
เพิ่ม Code นี้ด้านบนของ app/page.tsx ก่อน export default function Home():
type IssueStatus = "OPEN" | "IN_PROGRESS" | "DONE";
type Issue = {
id: string;
reporterName: string;
reporterEmail: string;
title: string;
description: string;
status: IssueStatus;
adminComment?: string;
createdAt: string;
};ทุก Property ใช้ string ยกเว้น status ที่ต้องเป็น IssueStatus
เครื่องหมาย ? ใน adminComment?: string หมายถึง Property นี้มีก็ได้ ไม่มีก็ได้ เพราะ Issue ใหม่อาจยังไม่มีความเห็นจาก Admin
Type Issue ยังไม่ได้สร้างข้อมูลจริง หน้าที่ของมันคือใช้ตรวจโครงสร้างของ Mock Data ที่เราจะสร้างใน Slide ถัดไป
Slide 7: สร้าง Mock Data รายการแรก
เพิ่ม issues ต่อจาก Type และก่อน Home():
const issues: Issue[] = [
{
id: "001",
reporterName: "Anan",
reporterEmail: "anan@example.com",
title: "Login เข้าระบบไม่ได้",
description: "ไม่สามารถเข้าสู่ระบบด้วยบัญชีเดิมได้",
status: "OPEN",
createdAt: "2026-05-08",
},
];Issue[] หมายถึง Array ที่สมาชิกทุกตัวต้องมีโครงสร้างตรงกับ Type Issue
ข้อมูลชุดนี้เป็น Mock Data สำหรับฝึกเขียน UI ยังไม่ได้มาจาก Form หรือ Database
Slide 8: เติมข้อมูลรายการที่สองและสาม
เพิ่ม Object สองตัวนี้ต่อจากรายการแรกและก่อน ];
{
id: "002",
reporterName: "Mali",
reporterEmail: "mali@example.com",
title: "ส่งแบบฟอร์มสมัครไม่ได้",
description: "กดส่งข้อมูลแล้วระบบขึ้น error",
status: "IN_PROGRESS",
createdAt: "2026-05-08",
},
{
id: "003",
reporterName: "Kanda",
reporterEmail: "kanda@example.com",
title: "ขอสิทธิ์เข้าใช้งาน Dashboard",
description: "ต้องการสิทธิ์สำหรับตรวจสอบข้อมูลหลังบ้าน",
status: "DONE",
adminComment: "อนุมัติสิทธิ์และแจ้งผู้ใช้เรียบร้อยแล้ว",
createdAt: "2026-05-08",
},รายการที่สามมี adminComment ส่วนสองรายการแรกไม่มี ซึ่งทำได้เพราะ Property นี้เป็น Optional
Slide 9: Type ช่วยตรวจ Mock Data ทุกตัว
เพราะเราประกาศ Array เป็น Issue[] TypeScript จะตรวจทุก Object ภายใน Array
ตัวอย่างนี้ลืม title:
const issues: Issue[] = [
{
id: "001",
reporterName: "Anan",
reporterEmail: "anan@example.com",
description: "ไม่สามารถเข้าสู่ระบบได้",
status: "OPEN",
createdAt: "2026-05-08",
},
];TypeScript จะแจ้งว่า Property title หายไป แต่ถ้าไม่ใส่ adminComment จะไม่ Error เพราะเรากำหนดไว้ว่าเป็น Optional
Slide 10: เปลี่ยน Static Rows เป็น .map()
ใน app/page.tsx ให้แทน <tbody> เดิมทั้งหมดด้วย Code นี้:
<tbody>
{issues.map((issue) => (
<tr key={issue.id}>
<td>#{issue.id}</td>
<td>{issue.title}</td>
<td>{issue.reporterName}</td>
<td>{issue.status}</td>
</tr>
))}
</tbody>ข้อความภายใน { } คือ JavaScript Expression ที่ใช้ใน TSX ได้ เช่น {issue.title} จะนำค่า title ของ Issue ปัจจุบันมาแสดง
ตอนนี้ให้แสดง {issue.status} เป็นข้อความธรรมดาก่อน เพื่อดูการทำงานของ .map() ให้ชัดเจน ส่วน <span className="status ..."> จะนำกลับมาครอบ Status ใน Slide 14 หลังจากเราสร้าง Function สำหรับเลือก Class แล้ว
Table List ไม่จำเป็นต้องแสดงทุก Field ส่วน description และ adminComment จะนำไปใช้ในหน้ารายละเอียดภายหลัง
Slide 11: .map() ทำงานทีละรายการ
เมื่อ Array มีสามรายการ .map() จะทำงานสามรอบ:
issues[0] → issue → <tr> ของ Anan
issues[1] → issue → <tr> ของ Mali
issues[2] → issue → <tr> ของ Kandaใน Code นี้:
issues.map((issue) => (
<tr>...</tr>
))issuesคือ Array ต้นทางissueคือข้อมูลหนึ่งรายการในรอบปัจจุบัน<tr>...</tr>คือผลลัพธ์ที่สร้างจากรายการนั้น
เมื่อเพิ่ม Object ใหม่ใน issues Table จะมี Row ใหม่ตามข้อมูลโดยไม่ต้อง Copy JSX เพิ่ม
Slide 12: ทำไมแต่ละ Row ต้องมี key
จาก Code ใน Slide ก่อนหน้า:
<tr key={issue.id}>React ใช้ key เพื่อแยกแต่ละรายการออกจากกัน เมื่อข้อมูลถูกเพิ่ม ลบ หรือเปลี่ยน React จะรู้ว่า Row ใดต้องอัปเดต
id เหมาะกับการเป็น key เพราะแต่ละ Issue มีค่าไม่ซ้ำกันและไม่เปลี่ยนตามลำดับของ Array
หากลืมใส่ key หน้าเว็บอาจยังแสดงได้ แต่ Browser Console จะแจ้ง Warning
Slide 13: แปลง Status เป็นชื่อ Class
เพิ่ม Function นี้ต่อจาก issues และก่อน Home():
function getStatusClass(status: IssueStatus): string {
if (status === "OPEN") {
return "status-open";
}
if (status === "IN_PROGRESS") {
return "status-progress";
}
return "status-done";
}Function รับ Status หนึ่งค่า แล้วคืนชื่อ Class ที่มีอยู่แล้วใน globals.css:
OPEN → status-open
IN_PROGRESS → status-progress
DONE → status-doneSlide 14: คืน Status Badge ให้แต่ละ Row
ภายใน .map() ให้แทน <td>{issue.status}</td> ด้วย:
<td>
<span className={`status ${getStatusClass(issue.status)}`}>
{issue.status}
</span>
</td>statusคือ Class พื้นฐานของ Badge${getStatusClass(issue.status)}เพิ่ม Class สีตาม Status{issue.status}แสดงข้อความ Status
ตอนนี้ทั้งข้อความและสีของ Badge มาจากข้อมูล Issue แต่ละรายการ ไม่ต้องเขียน Badge ซ้ำทีละ Row
Slide 15: ตรวจว่า Table สร้างจากข้อมูลแล้ว
ตรวจผลลัพธ์ใน Browser:
- Form จาก Hour 2 ยังแสดงเหมือนเดิม
- Table แสดงข้อมูลครบ 3 รายการ
- Status Badge แสดง
OPEN,IN_PROGRESSและDONE - Badge แต่ละสถานะมีสีถูกต้อง
- Terminal และ Browser ไม่มี Error หรือ Warning
ตรวจใน app/page.tsx:
IssueStatus,Issue,issuesและgetStatusClass()อยู่ก่อนHome()<tbody>ไม่มี Static Rows เดิม<tbody>ใช้issues.map(...)<tr>มีkey={issue.id}
ตอนนี้ UI สร้างจาก Mock Data แล้ว แต่ทุกอย่างยังอยู่ในไฟล์เดียว Hour ถัดไปเราจะแยก Type, Data, Form, List และ Badge ออกเป็นไฟล์ที่นำกลับมาใช้ซ้ำได้