Skip to content

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
  • แสดงรายชื่อนักเรียนที่ลงทะเบียนแล้ว
  1. สร้าง Zod Schema สำหรับ Registration Form

    fields: name (min 2), email (valid email), age (18-60), course (enum), acceptTerms (must be true)

  2. สร้าง Type จาก Schema

    ใช้ z.infer เพื่อสร้าง RegistrationForm type

  3. สร้าง Form Component

    ใช้ useState สำหรับ form data และ errors พร้อม event handlers ที่ typed

  4. เพิ่ม Validation Logic

    ใช้ safeParse เมื่อ submit พร้อมแสดง field-level errors

  5. แสดงรายชื่อ 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>
);
}