Lab: Full-stack Types
Lab: Full-stack Types
Section titled “Lab: Full-stack Types”ในแบบฝึกหัดสุดท้ายของ TypeScript track คุณจะสร้าง form ที่ type-safe ตลอดทั้ง pipeline — ตั้งแต่ Zod schema, form handling, API submission ไปจนถึงการแสดงผล
สร้างระบบลงทะเบียนนักเรียนที่มี:
- Zod schema สำหรับ form data
- React form component ที่ typed ทุก field
- Validation พร้อมแสดง error messages
- แสดงรายชื่อนักเรียนที่ลงทะเบียนแล้ว
-
สร้าง Zod Schema สำหรับ Registration Form
fields: name (min 2), email (valid email), age (18-60), course (enum), acceptTerms (must be true)
-
สร้าง Type จาก Schema
ใช้
z.inferเพื่อสร้างRegistrationFormtype -
สร้าง Form Component
ใช้
useStateสำหรับ form data และ errors พร้อม event handlers ที่ typed -
เพิ่ม Validation Logic
ใช้
safeParseเมื่อ submit พร้อมแสดง field-level errors -
แสดงรายชื่อ Registered Students
component ที่รับ
RegistrationForm[]แล้วแสดงเป็นตาราง
Show Solution
import { z } from "zod";
const RegistrationSchema = z.object({ name: z.string().min(2, "ชื่อต้องมีอย่างน้อย 2 ตัวอักษร"), email: z.string().email("รูปแบบอีเมลไม่ถูกต้อง"), age: z.number() .min(18, "อายุต้องไม่น้อยกว่า 18 ปี") .max(60, "อายุต้องไม่เกิน 60 ปี"), course: z.enum(["frontend", "backend", "fullstack", "data"], { errorMap: () => ({ message: "กรุณาเลือกหลักสูตร" }), }), acceptTerms: z.literal(true, { errorMap: () => ({ message: "ต้องยอมรับเงื่อนไข" }), }),});
type RegistrationForm = z.infer<typeof RegistrationSchema>;type FieldErrors = Partial<Record<keyof RegistrationForm, string>>;Show Solution
import { useState } from "react";
function RegistrationForm() { const [formData, setFormData] = useState({ name: "", email: "", age: "", course: "", acceptTerms: false, }); const [errors, setErrors] = useState<FieldErrors>({}); const [students, setStudents] = useState<RegistrationForm[]>([]);
const handleChange = ( e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> ) => { const { name, value, type } = e.target; setFormData(prev => ({ ...prev, [name]: type === "checkbox" ? (e.target as HTMLInputElement).checked : value, })); };
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault();
const dataToValidate = { ...formData, age: Number(formData.age), };
const result = RegistrationSchema.safeParse(dataToValidate);
if (!result.success) { const fieldErrors: FieldErrors = {}; result.error.issues.forEach(issue => { const field = issue.path[0] as keyof RegistrationForm; fieldErrors[field] = issue.message; }); setErrors(fieldErrors); return; }
setErrors({}); setStudents(prev => [...prev, result.data]); setFormData({ name: "", email: "", age: "", course: "", acceptTerms: false }); };
return ( <form onSubmit={handleSubmit}> <div> <label>ชื่อ</label> <input name="name" value={formData.name} onChange={handleChange} /> {errors.name && <span className="error">{errors.name}</span>} </div> <div> <label>อีเมล</label> <input name="email" value={formData.email} onChange={handleChange} /> {errors.email && <span className="error">{errors.email}</span>} </div> <div> <label>อายุ</label> <input name="age" type="number" value={formData.age} onChange={handleChange} /> {errors.age && <span className="error">{errors.age}</span>} </div> <div> <label>หลักสูตร</label> <select name="course" value={formData.course} onChange={handleChange}> <option value="">-- เลือก --</option> <option value="frontend">Frontend</option> <option value="backend">Backend</option> <option value="fullstack">Fullstack</option> <option value="data">Data</option> </select> {errors.course && <span className="error">{errors.course}</span>} </div> <div> <label> <input name="acceptTerms" type="checkbox" checked={formData.acceptTerms} onChange={handleChange} /> ยอมรับเงื่อนไข </label> {errors.acceptTerms && <span className="error">{errors.acceptTerms}</span>} </div> <button type="submit">ลงทะเบียน</button>
{students.length > 0 && <StudentList students={students} />} </form> );}Show Solution
interface StudentListProps { students: RegistrationForm[];}
function StudentList({ students }: StudentListProps) { return ( <div> <h3>นักเรียนที่ลงทะเบียนแล้ว ({students.length} คน)</h3> <table> <thead> <tr> <th>ชื่อ</th> <th>อีเมล</th> <th>อายุ</th> <th>หลักสูตร</th> </tr> </thead> <tbody> {students.map((student, index) => ( <tr key={index}> <td>{student.name}</td> <td>{student.email}</td> <td>{student.age}</td> <td>{student.course}</td> </tr> ))} </tbody> </table> </div> );}