FileEditViewTerminal
IT Issue Bootcamp - Visual Studio Code
day-2-hour-3.mdx

Day 2 / Hour 3

TypeScript Data Model

Issue, IssueStatus, mock data, mapping arrays, and data-driven rendering.

60 minutes
The issue list renders from typed mock data.

TypeScript Data Model

Day 2 - ชั่วโมงที่ 3: เปลี่ยน Static Rows ให้สร้างจากข้อมูล

เป้าหมายของชั่วโมงนี้

หลังจบชั่วโมงนี้ ผู้เรียนจะสามารถ:

  1. อธิบายได้ว่าทำไมเราควรแยกข้อมูลออกจาก JSX
  2. สร้าง Type สำหรับข้อมูล Issue ได้
  3. ใช้ Union Type จำกัดค่าของ Status ได้
  4. สร้าง Mock Data แบบ Issue[] ได้
  5. ใช้ .map() สร้าง Table Rows จากข้อมูลได้
  6. แสดง 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ระบบกำหนดหรืออัปเดต
reporterNameid
reporterEmailstatus
titlecreatedAt
descriptionadminComment

เราจะสร้าง 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-done

Slide 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 ออกเป็นไฟล์ที่นำกลับมาใช้ซ้ำได้