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: แยกข้อมูลออกจากโครงสร้าง Table
Hour นี้ไม่ได้เปลี่ยนหน้าตาของ Table แต่เปลี่ยนวิธีสร้างแต่ละแถว:
| ส่วน | ก่อนปรับ | หลังปรับ |
|---|---|---|
| ข้อมูลปัญหา | เขียนอยู่ใน <td> ของแต่ละแถว | เก็บรวมไว้ใน Array issues |
| โครงสร้างแถว | Copy <tr> พร้อมข้อมูลซ้ำทุกครั้ง | เขียน <tr> เป็นแบบสำหรับสร้างทุกแถว |
| เมื่อต้องเพิ่มปัญหา | Copy JSX แล้วแก้ข้อความ | เพิ่มข้อมูลหนึ่งรายการใน issues |
issues (ข้อมูล) → .map() → <tr> (หน้าตาที่แสดง).map() จึงเป็นตัวนำข้อมูลแต่ละรายการจาก issues มาสร้างเป็นหนึ่งแถว โดยรายละเอียดการเขียน Code จะเริ่มหลังจากเรากำหนดรูปแบบข้อมูลด้วย TypeScript
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 เพียงสามค่า:
type IssueStatus = "OPEN" | "IN_PROGRESS" | "DONE";
เครื่องหมาย | อ่านว่า “หรือ” ดังนั้น IssueStatus จึงรับได้เฉพาะ:
"OPEN""IN_PROGRESS""DONE"
หากใส่ค่าอื่น เช่น "FINISHED" TypeScript จะแจ้งเตือนก่อนเปิดหน้าเว็บ
Slide 6: สร้าง Type Issue
Issue กำหนดว่า Issue หนึ่งรายการต้องมี Property ใดและแต่ละค่าต้องเป็นชนิดอะไร
type Issue = { id: string; reporterName: string; reporterEmail: string; title: string; description: string; status: IssueStatus; adminComment?: string; createdAt: string;};
statusรับได้เฉพาะค่าจากIssueStatusadminComment?เป็น Optional Property เพราะ Issue ใหม่อาจยังไม่มีความเห็นจาก Admin
Slide 7: สร้าง Mock Data รายการแรก
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: เพิ่ม Mock Data รายการที่สอง
{ id: "002", reporterName: "Mali", reporterEmail: "mali@example.com", title: "ส่งแบบฟอร์มไม่ได้", description: "กดส่งข้อมูลแล้วระบบขึ้น error", status: "IN_PROGRESS", createdAt: "2026-05-08",},
รายการนี้ใช้ status: "IN_PROGRESS" ซึ่ง TypeScript ตรวจว่าเป็นค่าที่ IssueStatus รองรับ
Slide 9: เพิ่มรายการที่สามและ Optional Property
{ id: "003", reporterName: "Kanda", reporterEmail: "kanda@example.com", title: "ขอสิทธิ์เข้า Dashboard", description: "ต้องการสิทธิ์สำหรับตรวจสอบข้อมูลหลังบ้าน", status: "DONE", adminComment: "อนุมัติสิทธิ์และแจ้งผู้ใช้เรียบร้อยแล้ว", createdAt: "2026-05-08",},
รายการที่สามมี adminComment ส่วนสองรายการแรกไม่มี ซึ่งทำได้เพราะ Property นี้เป็น Optional Property
Slide 10: 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 11: เปลี่ยน Static Rows เป็น .map()
<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 12: .map() ทำงานทีละรายการ
เมื่อ Array มีสามรายการ .map() จะทำงานสามรอบ:
issues[0] → issue → <tr> ของ Anan
issues[1] → issue → <tr> ของ Mali
issues[2] → issue → <tr> ของ Kandaissuesคือ Array ต้นทางissueคือข้อมูลหนึ่งรายการในรอบปัจจุบัน<tr>...</tr>คือผลลัพธ์ที่สร้างจากรายการนั้น
เมื่อเพิ่ม Object ใหม่ใน issues Table จะมี Row ใหม่ตามข้อมูลโดยไม่ต้อง Copy JSX เพิ่ม
Slide 13: ทำไมแต่ละ Row ต้องมี key
จาก Code ใน Slide ก่อนหน้า:
<tr key={issue.id}>React ใช้ key เพื่อแยกแต่ละรายการออกจากกัน เมื่อข้อมูลถูกเพิ่ม ลบ หรือเปลี่ยน React จะรู้ว่า Row ใดต้องอัปเดต
id เหมาะกับการเป็น key เพราะแต่ละ Issue มีค่าไม่ซ้ำกันและไม่เปลี่ยนตามลำดับของ Array
หากลืมใส่ key หน้าเว็บอาจยังแสดงได้ แต่ Browser Console จะแจ้ง Warning
Slide 14: แปลง Status เป็นชื่อ Class
function getStatusClass(status: IssueStatus): string { if (status === "OPEN") { return "status-open"; } if (status === "IN_PROGRESS") { return "status-progress"; } return "status-done";}
Function รับ Status หนึ่งค่า แล้วคืน status-open, status-progress หรือ status-done ที่มีอยู่แล้วใน globals.css
Slide 15: คืน Status Badge ให้แต่ละ Row
<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 16: ตรวจว่า Table สร้างจากข้อมูลแล้ว
ตรวจผลลัพธ์ใน Browser และ app/page.tsx:
- Form จาก Hour 2 ยังแสดงเหมือนเดิม
- Table แสดงข้อมูลครบ 3 รายการและ Badge มีสีถูกต้อง
- Terminal และ Browser ไม่มี Error หรือ Warning
IssueStatus,Issue,issuesและgetStatusClass()อยู่ก่อนHome()<tbody>ไม่มี Static Rows เดิมและใช้issues.map(...)- ทุก
<tr>มีkey={issue.id}
ตอนนี้ UI สร้างจาก Mock Data แล้ว แต่ทุกอย่างยังอยู่ในไฟล์เดียว Hour ถัดไปเราจะแยก Type, Data, Form, List และ Badge ออกเป็นไฟล์ที่นำกลับมาใช้ซ้ำได้